- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
Finance
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Playwright
- Prettier
- Block Rebound
Sales & GTM
all.tsx and featured.tsx are client view leaves that render data via card variants from card.tsx. all.tsx is grid/list with filtering; featured.tsx exports multiple curated views (Featured, Recent, RequiringAttention, Stats).
Two patterns
| Pattern | Purpose | Reference |
|---|---|---|
all.tsx | Full listing with search, filters, sort, layout toggle | saas-dashboard/domains/all.tsx |
featured.tsx | Multiple curated slices — Featured, Recent, Stats | saas-dashboard/billing/featured.tsx |
Rules
| # | Rule |
|---|---|
| 1 | "use client" — views manage filter / sort / layout state |
| 2 | Import card variants from card.tsx — never inline card markup |
| 3 | useMemo for client-side filtering |
| 4 | all.tsx provides grid/list toggle |
| 5 | Empty state with "Clear filters" button |
Canonical example — all.tsx
"use client"
import { useMemo, useState } from "react"
import { Globe, Grid3x3, List, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select"
import { DomainCard, DomainCompactCard } from "./card"
import { DOMAIN_STATUSES } from "./config"
import type { DomainRequestWithSchool } from "./types"
import { sortDomains } from "./util"
interface AllDomainsProps {
domains: DomainRequestWithSchool[]
defaultView?: "grid" | "list"
}
export function AllDomains({ domains, defaultView = "grid" }: AllDomainsProps) {
const [view, setView] = useState<"grid" | "list">(defaultView)
const [search, setSearch] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("")
const filteredDomains = useMemo(() => {
let result = domains
if (search) {
const q = search.toLowerCase()
result = result.filter(
(d) =>
d.domain.toLowerCase().includes(q) ||
d.school.name.toLowerCase().includes(q)
)
}
if (statusFilter) result = result.filter((d) => d.status === statusFilter)
return sortDomains(result, "createdAt", "desc")
}, [domains, search, statusFilter])
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center gap-4">
<Input
placeholder="Search domains..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-40"><SelectValue placeholder="All" /></SelectTrigger>
<SelectContent>
<SelectItem value="">All</SelectItem>
{DOMAIN_STATUSES.map((s) => (
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex gap-1">
<Button variant={view === "grid" ? "default" : "ghost"} size="icon" onClick={() => setView("grid")}>
<Grid3x3 className="size-4" />
</Button>
<Button variant={view === "list" ? "default" : "ghost"} size="icon" onClick={() => setView("list")}>
<List className="size-4" />
</Button>
</div>
</div>
{filteredDomains.length === 0 ? (
<div className="flex flex-col items-center py-12 text-center">
<Globe className="text-muted-foreground mb-4 size-12" />
<h3>No domains found</h3>
<Button variant="outline" onClick={() => { setSearch(""); setStatusFilter("") }}>
Clear Filters
</Button>
</div>
) : view === "grid" ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredDomains.map((d) => <DomainCard key={d.id} domain={d} />)}
</div>
) : (
<div className="space-y-2">
{filteredDomains.map((d) => <DomainCompactCard key={d.id} domain={d} />)}
</div>
)}
</div>
)
}featured.tsx — multiple curated exports
"use client"
import { InvoiceCard, InvoiceStatsCard } from "./card"
import { isInvoiceOverdue } from "./util"
export function FeaturedInvoices({ invoices, maxItems = 6 }) {
const featured = invoices.slice(0, maxItems)
return <div className="grid gap-4 md:grid-cols-3">{featured.map((i) => <InvoiceCard key={i.id} invoice={i} />)}</div>
}
export function RecentInvoices({ invoices, maxItems = 5 }) {
const recent = [...invoices]
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.slice(0, maxItems)
return <div className="space-y-2">{recent.map((i) => <InvoiceCard key={i.id} invoice={i} compact />)}</div>
}
export function RequiringAttention({ invoices }) {
const overdue = invoices.filter((i) => isInvoiceOverdue(i.periodEnd, i.status))
if (overdue.length === 0) return null
return <div>{/* ... */}</div>
}
export function OverviewStats({ invoices }) {
const totalRevenue = invoices.reduce((sum, i) => sum + i.amountPaid, 0)
return <InvoiceStatsCard total={totalRevenue} />
}Server-side vs client-side filtering
| Dataset Size | Approach | Pattern |
|---|---|---|
| Large (100+) | Server-side (preferred) | table.tsx with URL pagination |
| Small (< 100) | Client-side acceptable | all.tsx with useMemo |
| Mixed | Server initial, client refinement | Hybrid |
For URL state syncing, prefer nuqs useQueryState over useState — see List Params.
Anti-patterns
- Card markup inline in
all.tsx— import fromcard.tsx. - Filtering without
useMemo— recalculates every render. - Missing empty state — users think it's loading.
File layout
src/components/<feature>/
config.ts # Filter options
types.ts # Entity types
util.ts # Sort, filter, format
card.tsx # Card variants (imported by views)
all.tsx # Grid/list with filtering
featured.tsx # Curated views
content.tsx # Server component (passes data)