UI components and patterns in the FindU web app
/components/ui/
ui/ ├── button.tsx # Buttons with variants ├── card.tsx # Container components ├── dialog.tsx # Modal dialogs ├── input.tsx # Form inputs ├── select.tsx # Dropdown selects ├── table.tsx # Data tables └── ... # 30+ components
components/ ├── page-header.tsx # Consistent page headers ├── data-displays/ # Charts and visualizations ├── app-sidebar.tsx # Navigation sidebar └── team-switcher.tsx # Multi-entity selector
import { Button } from "@/components/ui/button" // Primary button <Button>Save Changes</Button> // Secondary variant <Button variant="secondary">Cancel</Button> // Destructive action <Button variant="destructive">Delete</Button> // Ghost button (no background) <Button variant="ghost">Learn More</Button> // With icon <Button> <PlusIcon className="mr-2 h-4 w-4" /> Add New </Button>
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card" <Card> <CardHeader> <CardTitle>Analytics Overview</CardTitle> </CardHeader> <CardContent> <p>Your content here</p> </CardContent> </Card>
import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" // Text input <div className="space-y-2"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="partner@school.edu" /> </div> // Select dropdown <Select> <SelectTrigger> <SelectValue placeholder="Choose timeframe" /> </SelectTrigger> <SelectContent> <SelectItem value="30">Last 30 days</SelectItem> <SelectItem value="90">Last 90 days</SelectItem> </SelectContent> </Select>
import { PageHeader } from "@/components/page-header" <PageHeader title="Campaign Analytics" description="Track student engagement with your school" actions={ <Button>Export Data</Button> } />
import { StatsCard } from "@/components/data-displays/stats-card" <StatsCard title="Total Students" value={1234} change={+12.5} changeLabel="vs last month" />
import { EngagementChart } from "@/components/data-displays/engagement-chart" <EngagementChart data={chartData} timeframe="30d" onTimeframeChange={(tf) => setTimeframe(tf)} />
import { Skeleton } from "@/components/ui/skeleton" // Loading cards <div className="grid gap-4 md:grid-cols-3"> {[1, 2, 3].map((i) => ( <Card key={i}> <CardHeader> <Skeleton className="h-4 w-[150px]" /> </CardHeader> <CardContent> <Skeleton className="h-8 w-[100px]" /> </CardContent> </Card> ))} </div>
export function StudentTable() { return ( <div className="space-y-4"> <div className="flex items-center justify-between"> <StudentFilters /> <Button> <Download className="mr-2 h-4 w-4" /> Export </Button> </div> <Table> <TableHeader> <TableRow> <TableHead>Student</TableHead> <TableHead>Match Score</TableHead> <TableHead>Interaction</TableHead> <TableHead>Date</TableHead> </TableRow> </TableHeader> <TableBody> {students.map((student) => ( <TableRow key={student.id}> <TableCell>{student.name}</TableCell> <TableCell> <Badge variant={getScoreVariant(student.score)}> {student.score}% </Badge> </TableCell> <TableCell>{student.interaction}</TableCell> <TableCell>{formatDate(student.date)}</TableCell> </TableRow> ))} </TableBody> </Table> </div> ); }
// Mobile-first responsive classes <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> {/* 1 column mobile, 2 tablet, 3 desktop */} </div> // Conditional rendering for mobile <Button className="hidden md:inline-flex"> Desktop Only Action </Button>
// Consistent spacing and colors <Card className="p-6 space-y-4 border-muted"> <h3 className="text-lg font-semibold text-foreground"> Title </h3> <p className="text-sm text-muted-foreground"> Description text </p> </Card>
--background
--foreground
--muted
--primary
--destructive
interface PageHeaderProps { /** Main title of the page */ title: string; /** Optional description text */ description?: string; /** Action buttons to display */ actions?: React.ReactNode; /** Additional classes */ className?: string; }
import { render, screen } from '@testing-library/react' import { Button } from '@/components/ui/button' test('renders button with text', () => { render(<Button>Click me</Button>) expect(screen.getByRole('button')).toHaveTextContent('Click me') })
export default { title: 'UI/Button', component: Button, } export const Primary = { args: { children: 'Button', }, }