0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • 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
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Views

PreviousNext

Grid / list / curated view components composed from card variants.

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

PatternPurposeReference
all.tsxFull listing with search, filters, sort, layout togglesaas-dashboard/domains/all.tsx
featured.tsxMultiple curated slices — Featured, Recent, Statssaas-dashboard/billing/featured.tsx

Rules

#Rule
1"use client" — views manage filter / sort / layout state
2Import card variants from card.tsx — never inline card markup
3useMemo for client-side filtering
4all.tsx provides grid/list toggle
5Empty 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 SizeApproachPattern
Large (100+)Server-side (preferred)table.tsx with URL pagination
Small (< 100)Client-side acceptableall.tsx with useMemo
MixedServer initial, client refinementHybrid

For URL state syncing, prefer nuqs useQueryState over useState — see List Params.

Anti-patterns

  • Card markup inline in all.tsx — import from card.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)

See also

  • Pattern
  • Card
  • Util
  • Table
  • List Params
List ParamsREADME.md

On This Page

Two patternsRulesCanonical example — all.tsxfeatured.tsx — multiple curated exportsServer-side vs client-side filteringAnti-patternsFile layoutSee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.