Getting Started
Components
Search for a command to run...
Comprehensive code examples referenced from the Design Standards documentation. Use these as templates when implementing features.
Use for auth, tenant selection, theme, and other session-level state.
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
type Tenant = {
readonly id: string;
readonly name: string;
};
type TenantContextValue = {
readonly tenants: Tenant[];
readonly selectedTenant: Tenant | null;
readonly setSelectedTenant: (tenant: Tenant) => void;
readonly isLoading: boolean;
};
const TenantContext = createContext<TenantContextValue | null>(null);
export function TenantProvider({ children }: { readonly children: ReactNode }) {
const [tenants, setTenants] = useState<Tenant[]>([]);
const [selectedTenant, setSelectedTenant] = useState<Tenant | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Load from localStorage on mount
useEffect(() => {
const savedTenantId = localStorage.getItem('selectedTenantId');
// ... initialization logic
setIsLoading(false);
}, []);
// Persist selection
useEffect(() => {
if (selectedTenant) {
localStorage.setItem('selectedTenantId', selectedTenant.id);
}
}, [selectedTenant]);
return (
<TenantContext.Provider
value={{ tenants, selectedTenant, setSelectedTenant, isLoading }}
>
{children}
</TenantContext.Provider>
);
}
export function useTenantContext() {
const context = useContext(TenantContext);
if (!context) {
throw new Error('useTenantContext must be used within TenantProvider');
}
return context;
}Use for cross-component state, computed values, or state needing middleware.
import { create } from "zustand"
import { persist } from "zustand/middleware"
type ProjectStatus = "active" | "archived" | "draft"
type SortField = "name" | "createdAt" | "updatedAt"
type SortOrder = "asc" | "desc"
type ProjectFiltersState = {
// State
search: string
statuses: ProjectStatus[]
sortField: SortField
sortOrder: SortOrder
// Computed
hasActiveFilters: () => boolean
// Actions
setSearch: (search: string) => void
toggleStatus: (status: ProjectStatus) => void
setSort: (field: SortField, order: SortOrder) => void
resetFilters: () => void
}
const DEFAULT_STATE = {
search: "",
statuses: [] as ProjectStatus[],
sortField: "updatedAt" as SortField,
sortOrder: "desc" as SortOrder,
}
export const useProjectFiltersStore = create<ProjectFiltersState>()(
persist(
(set, get) => ({
...DEFAULT_STATE,
hasActiveFilters: () => {
const state = get()
return state.search !== "" || state.statuses.length > 0
},
setSearch: (search) => set({ search }),
toggleStatus: (status) =>
set((state) => ({
statuses: state.statuses.includes(status)
? state.statuses.filter((s) => s !== status)
: [...state.statuses, status],
})),
setSort: (sortField, sortOrder) => set({ sortField, sortOrder }),
resetFilters: () => set(DEFAULT_STATE),
}),
{
name: "project-filters",
partialize: (state) => ({
sortField: state.sortField,
sortOrder: state.sortOrder,
}),
}
)
)// BAD - Don't use Zustand for simple component state
const useModalStore = create((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}));
// GOOD - Use useState for component-local UI state
function ProjectCard() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<Button onClick={() => setIsModalOpen(true)}>Edit</Button>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
{/* ... */}
</Dialog>
</>
);
}export const queryKeys = {
// Projects
projects: {
all: ["projects"] as const,
lists: () => [...queryKeys.projects.all, "list"] as const,
list: (filters: { tenantId?: string; status?: string }) =>
[...queryKeys.projects.lists(), filters] as const,
details: () => [...queryKeys.projects.all, "detail"] as const,
detail: (id: string) => [...queryKeys.projects.details(), id] as const,
},
// Comments
comments: {
all: ["comments"] as const,
byThread: (threadId: string) =>
[...queryKeys.comments.all, "thread", threadId] as const,
byProject: (projectId: string) =>
[...queryKeys.comments.all, "project", projectId] as const,
},
// Users
users: {
all: ["users"] as const,
current: () => [...queryKeys.users.all, "current"] as const,
list: (filters?: { role?: string }) =>
[...queryKeys.users.all, "list", filters] as const,
detail: (id: string) => [...queryKeys.users.all, "detail", id] as const,
},
// Lookups (reference data)
lookups: {
all: ["lookups"] as const,
projectTypes: () => [...queryKeys.lookups.all, "projectTypes"] as const,
statuses: () => [...queryKeys.lookups.all, "statuses"] as const,
},
}import { useQuery } from "@tanstack/react-query"
import { projectsApi } from "@/lib/api/projects-api"
import { useTenantContext } from "@/lib/tenant-context"
import { queryKeys } from "./query-keys"
import { useAuthReady } from "./use-auth-ready"
export function useProjects() {
const { selectedTenant } = useTenantContext()
const isAuthReady = useAuthReady()
return useQuery({
queryKey: queryKeys.projects.list({ tenantId: selectedTenant?.id }),
queryFn: projectsApi.getAll,
enabled: isAuthReady && !!selectedTenant,
})
}
export function useProject(id: string) {
const isAuthReady = useAuthReady()
return useQuery({
queryKey: queryKeys.projects.detail(id),
queryFn: () => projectsApi.getById(id),
enabled: isAuthReady && !!id,
})
}import { keepPreviousData, useQuery } from "@tanstack/react-query"
import { lookupApi } from "@/lib/api/lookup-api"
import { queryKeys } from "./query-keys"
const LOOKUP_CONFIG = {
staleTime: 30 * 60 * 1000, // 30 minutes
gcTime: 60 * 60 * 1000, // 1 hour
placeholderData: keepPreviousData,
refetchOnWindowFocus: false,
refetchOnMount: false,
}
export function useProjectTypes() {
return useQuery({
queryKey: queryKeys.lookups.projectTypes(),
queryFn: lookupApi.getProjectTypes,
...LOOKUP_CONFIG,
})
}
export function useStatuses() {
return useQuery({
queryKey: queryKeys.lookups.statuses(),
queryFn: lookupApi.getStatuses,
...LOOKUP_CONFIG,
})
}import { useMutation, useQueryClient } from "@tanstack/react-query"
import {
projectsApi,
type CreateProjectRequest,
type ProjectResponse,
} from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
import { queryKeys } from "./query-keys"
export function useCreateProject() {
const queryClient = useQueryClient()
const { selectedTenant } = useTenantContext()
return useMutation({
mutationFn: (data: CreateProjectRequest) => projectsApi.create(data),
onMutate: async (newProject) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: queryKeys.projects.lists(),
})
// Snapshot the previous value
const previousProjects = queryClient.getQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id })
)
// Optimistically update the cache
queryClient.setQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
(old) => [
...(old ?? []),
{
...newProject,
id: `temp-${Date.now()}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
} as ProjectResponse,
]
)
// Return context with the snapshot
return { previousProjects }
},
onSuccess: (data) => {
toast({
title: "Project created",
description: `"${data.name}" has been created successfully.`,
})
// Invalidate to refetch with real data
queryClient.invalidateQueries({
queryKey: queryKeys.projects.lists(),
})
},
onError: (error, _variables, context) => {
// Rollback to the previous value
if (context?.previousProjects) {
queryClient.setQueryData(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
context.previousProjects
)
}
toast({
title: "Failed to create project",
description: getUserMessage(error),
variant: "destructive",
})
},
})
}import { useMutation, useQueryClient } from "@tanstack/react-query"
import {
projectsApi,
type ProjectResponse,
type UpdateProjectRequest,
} from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
import { queryKeys } from "./query-keys"
export function useUpdateProject(projectId: string) {
const queryClient = useQueryClient()
const { selectedTenant } = useTenantContext()
return useMutation({
mutationFn: (data: UpdateProjectRequest) =>
projectsApi.update(projectId, data),
onMutate: async (updates) => {
// Cancel related queries
await queryClient.cancelQueries({
queryKey: queryKeys.projects.detail(projectId),
})
await queryClient.cancelQueries({
queryKey: queryKeys.projects.lists(),
})
// Snapshot previous values
const previousProject = queryClient.getQueryData<ProjectResponse>(
queryKeys.projects.detail(projectId)
)
const previousList = queryClient.getQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id })
)
// Optimistically update detail cache
if (previousProject) {
queryClient.setQueryData<ProjectResponse>(
queryKeys.projects.detail(projectId),
{
...previousProject,
...updates,
updatedAt: new Date().toISOString(),
}
)
}
// Optimistically update list cache
if (previousList) {
queryClient.setQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
previousList.map((p) =>
p.id === projectId
? { ...p, ...updates, updatedAt: new Date().toISOString() }
: p
)
)
}
return { previousProject, previousList }
},
onSuccess: () => {
toast({ title: "Project updated" })
queryClient.invalidateQueries({
queryKey: queryKeys.projects.detail(projectId),
})
queryClient.invalidateQueries({
queryKey: queryKeys.projects.lists(),
})
},
onError: (error, _variables, context) => {
// Rollback
if (context?.previousProject) {
queryClient.setQueryData(
queryKeys.projects.detail(projectId),
context.previousProject
)
}
if (context?.previousList) {
queryClient.setQueryData(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
context.previousList
)
}
toast({
title: "Failed to update project",
description: getUserMessage(error),
variant: "destructive",
})
},
})
}import { useMutation, useQueryClient } from "@tanstack/react-query"
import { projectsApi, type ProjectResponse } from "@/lib/api/projects-api"
import { getUserMessage } from "@/lib/error-messages"
import { useTenantContext } from "@/lib/tenant-context"
import { toast } from "@/hooks/use-toast"
import { queryKeys } from "./query-keys"
export function useDeleteProject() {
const queryClient = useQueryClient()
const { selectedTenant } = useTenantContext()
return useMutation({
mutationFn: (projectId: string) => projectsApi.delete(projectId),
onMutate: async (projectId) => {
await queryClient.cancelQueries({
queryKey: queryKeys.projects.lists(),
})
const previousList = queryClient.getQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id })
)
// Optimistically remove from list
queryClient.setQueryData<ProjectResponse[]>(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
(old) => old?.filter((p) => p.id !== projectId) ?? []
)
return { previousList }
},
onSuccess: (_data, projectId) => {
toast({ title: "Project deleted" })
// Remove from detail cache
queryClient.removeQueries({
queryKey: queryKeys.projects.detail(projectId),
})
},
onError: (error, _projectId, context) => {
if (context?.previousList) {
queryClient.setQueryData(
queryKeys.projects.list({ tenantId: selectedTenant?.id }),
context.previousList
)
}
toast({
title: "Failed to delete project",
description: getUserMessage(error),
variant: "destructive",
})
},
})
}import { useQueryClient } from "@tanstack/react-query"
import { projectsApi } from "@/lib/api/projects-api"
import { queryKeys } from "@/hooks/query-keys"
function ProjectCard({ project }: { readonly project: ProjectResponse }) {
const queryClient = useQueryClient()
const handleMouseEnter = () => {
// Prefetch project details on hover for instant navigation
queryClient.prefetchQuery({
queryKey: queryKeys.projects.detail(project.id),
queryFn: () => projectsApi.getById(project.id),
staleTime: 5 * 60 * 1000, // 5 minutes
})
}
return (
<Link to={`/projects/${project.id}`} onMouseEnter={handleMouseEnter}>
<Card>
<CardHeader>
<CardTitle>{project.name}</CardTitle>
</CardHeader>
</Card>
</Link>
)
}Use for 1-3 fields with no validation.
import { useState } from "react"
import { Search } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function QuickSearch({
onSearch,
}: {
readonly onSearch: (query: string) => void
}) {
const [query, setQuery] = useState("")
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (query.trim()) {
onSearch(query.trim())
}
}
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
aria-label="Search query"
/>
<Button type="submit" size="icon" aria-label="Search">
<Search className="h-4 w-4" />
</Button>
</form>
)
}Use for 4+ fields or when validation is required.
import { z } from "zod"
export const createProjectSchema = z.object({
name: z
.string()
.min(1, "Project name is required")
.max(100, "Project name must be 100 characters or less"),
type: z.enum(["internal", "external", "research"], {
required_error: "Please select a project type",
}),
description: z
.string()
.max(500, "Description must be 500 characters or less")
.optional(),
startDate: z.date({
required_error: "Start date is required",
}),
budget: z.number().min(0, "Budget must be a positive number").optional(),
teamMembers: z
.array(z.string())
.min(1, "At least one team member is required"),
})
export type CreateProjectInput = z.infer<typeof createProjectSchema>
// Update schema (all fields optional for partial updates)
export const updateProjectSchema = createProjectSchema.partial()
export type UpdateProjectInput = z.infer<typeof updateProjectSchema>import {
createProjectSchema,
type CreateProjectInput,
} from "@/schemas/project-schema"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useCreateProject } from "@/hooks/use-create-project"
import { useProjectTypes } from "@/hooks/use-lookups"
import { Button } from "@/components/ui/button"
import { DatePicker } from "@/components/ui/date-picker"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { MultiSelect } from "@/components/ui/multi-select"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
type CreateProjectFormProps = {
readonly onSuccess?: () => void
readonly onCancel?: () => void
}
export function CreateProjectForm({
onSuccess,
onCancel,
}: CreateProjectFormProps) {
const { mutate: createProject, isPending } = useCreateProject()
const { data: projectTypes } = useProjectTypes()
const form = useForm<CreateProjectInput>({
resolver: zodResolver(createProjectSchema),
defaultValues: {
name: "",
type: undefined,
description: "",
startDate: undefined,
budget: undefined,
teamMembers: [],
},
})
const onSubmit = (data: CreateProjectInput) => {
createProject(data, {
onSuccess: () => {
form.reset()
onSuccess?.()
},
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Text Input */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Project Name</FormLabel>
<FormControl>
<Input placeholder="Enter project name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select */}
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Project Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a project type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{projectTypes?.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Describe the project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
Optional. Maximum 500 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Date Picker */}
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Start Date</FormLabel>
<DatePicker date={field.value} onDateChange={field.onChange} />
<FormMessage />
</FormItem>
)}
/>
{/* Number Input */}
<FormField
control={form.control}
name="budget"
render={({ field }) => (
<FormItem>
<FormLabel>Budget</FormLabel>
<FormControl>
<Input
type="number"
placeholder="0.00"
{...field}
onChange={(e) =>
field.onChange(e.target.valueAsNumber || undefined)
}
/>
</FormControl>
<FormDescription>Optional. Enter amount in USD.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Multi-Select */}
<FormField
control={form.control}
name="teamMembers"
render={({ field }) => (
<FormItem>
<FormLabel>Team Members</FormLabel>
<FormControl>
<MultiSelect
selected={field.value}
onChange={field.onChange}
placeholder="Select team members"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Actions */}
<div className="flex justify-end gap-3">
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel}>
Cancel
</Button>
)}
<Button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create Project"}
</Button>
</div>
</form>
</Form>
)
}export class ApiError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly code: string = "UNKNOWN_ERROR"
) {
super(message)
this.name = "ApiError"
}
}
type ApiResponse<T> = {
success: boolean
message?: string
data: T
error?: {
code: string
message: string
}
}
class ApiClient {
private baseUrl: string
private getAccessToken: (() => string | null) | null = null
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
setAccessTokenGetter(getter: () => string | null) {
this.getAccessToken = getter
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}/${endpoint}`
const token = this.getAccessToken?.()
const headers: HeadersInit = {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
}
try {
const response = await fetch(url, { ...options, headers })
const data: ApiResponse<T> = await response.json()
if (!response.ok || !data.success) {
throw new ApiError(
data.error?.message ?? data.message ?? "Request failed",
response.status,
data.error?.code ?? "API_ERROR"
)
}
return data.data
} catch (error) {
if (error instanceof ApiError) {
throw error
}
// Network error
throw new ApiError("Unable to connect to server", 0, "NETWORK_ERROR")
}
}
get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: "GET" })
}
post<T>(endpoint: string, body: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: "POST",
body: JSON.stringify(body),
})
}
put<T>(endpoint: string, body: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: "PUT",
body: JSON.stringify(body),
})
}
delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: "DELETE" })
}
}
export const api = new ApiClient(import.meta.env.VITE_API_BASE_URL)import type { ApiError } from "./api-client"
const ERROR_MESSAGES: Record<string, string> = {
// Authentication
UNAUTHORIZED: "Your session has expired. Please sign in again.",
FORBIDDEN: "You do not have permission to perform this action.",
// Validation
VALIDATION_ERROR: "Please check your input and try again.",
INVALID_INPUT: "The provided data is invalid.",
// Resources
NOT_FOUND: "The requested item could not be found.",
ALREADY_EXISTS: "An item with this name already exists.",
CONFLICT: "This item has been modified. Please refresh and try again.",
// Business logic
TENANT_REQUIRED: "Please select a tenant first.",
PROJECT_ARCHIVED: "This project is archived and cannot be modified.",
LIMIT_EXCEEDED: "You have reached the maximum limit for this resource.",
// Network
NETWORK_ERROR: "Unable to connect. Please check your internet connection.",
TIMEOUT: "The request timed out. Please try again.",
// Server
INTERNAL_ERROR: "An unexpected error occurred. Please try again later.",
SERVICE_UNAVAILABLE: "The service is temporarily unavailable.",
// Default
DEFAULT: "Something went wrong. Please try again.",
}
export function getUserMessage(error: unknown): string {
if (error instanceof Error && "code" in error) {
const apiError = error as ApiError
return ERROR_MESSAGES[apiError.code] ?? ERROR_MESSAGES.DEFAULT
}
if (error instanceof Error) {
return error.message
}
return ERROR_MESSAGES.DEFAULT
}
// For specific error handling
export function isAuthError(error: unknown): boolean {
if (error instanceof Error && "code" in error) {
const code = (error as ApiError).code
return code === "UNAUTHORIZED" || code === "FORBIDDEN"
}
return false
}
export function isNetworkError(error: unknown): boolean {
if (error instanceof Error && "code" in error) {
return (error as ApiError).code === "NETWORK_ERROR"
}
return false
}import { Component, type ReactNode } from "react"
import { AlertTriangle } from "lucide-react"
import { Button } from "@/components/ui/button"
type ErrorBoundaryProps = {
readonly children: ReactNode
readonly fallback?: ReactNode
}
type ErrorBoundaryState = {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log to error reporting service
console.error("Error caught by boundary:", error, errorInfo)
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback
}
return (
<div className="flex min-h-[400px] flex-col items-center justify-center p-8">
<AlertTriangle className="text-destructive h-12 w-12" />
<h2 className="mt-4 text-xl font-semibold">Something went wrong</h2>
<p className="text-muted-foreground mt-2 text-center">
An unexpected error occurred. Please try refreshing the page.
</p>
<div className="mt-6 flex gap-3">
<Button variant="outline" onClick={() => window.location.reload()}>
Refresh Page
</Button>
<Button onClick={this.handleReset}>Try Again</Button>
</div>
</div>
)
}
return this.props.children
}
}import { AlertCircle, RefreshCw } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type ErrorMessageProps = {
readonly title?: string
readonly message: string
readonly onRetry?: () => void
readonly className?: string
}
export function ErrorMessage({
title = "Error",
message,
onRetry,
className,
}: ErrorMessageProps) {
return (
<div
className={cn(
"flex flex-col items-center justify-center py-8 text-center",
className
)}
>
<AlertCircle className="text-destructive h-12 w-12" />
<h3 className="mt-4 text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground mt-2 max-w-md text-sm">{message}</p>
{onRetry && (
<Button variant="outline" onClick={onRetry} className="mt-4">
<RefreshCw className="mr-2 h-4 w-4" />
Try Again
</Button>
)}
</div>
)
}import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
export function ProjectCardSkeleton() {
return (
<Card>
<CardHeader className="space-y-2">
<Skeleton className="h-6 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</CardHeader>
<CardContent className="space-y-3">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<div className="flex gap-2 pt-2">
<Skeleton className="h-6 w-16 rounded-full" />
<Skeleton className="h-6 w-20 rounded-full" />
</div>
</CardContent>
</Card>
)
}
export function ProjectGridSkeleton({
count = 6,
}: {
readonly count?: number
}) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: count }).map((_, i) => (
<ProjectCardSkeleton key={i} />
))}
</div>
)
}import { Skeleton } from "@/components/ui/skeleton"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
type TableSkeletonProps = {
readonly columns: number
readonly rows?: number
}
export function TableSkeleton({ columns, rows = 5 }: TableSkeletonProps) {
return (
<Table>
<TableHeader>
<TableRow>
{Array.from({ length: columns }).map((_, i) => (
<TableHead key={i}>
<Skeleton className="h-4 w-24" />
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, rowIndex) => (
<TableRow key={rowIndex}>
{Array.from({ length: columns }).map((_, colIndex) => (
<TableCell key={colIndex}>
<Skeleton className="h-4 w-full" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}import type { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
type EmptyStateProps = {
readonly icon: LucideIcon
readonly title: string
readonly description: string
readonly action?: React.ReactNode
readonly className?: string
}
export function EmptyState({
icon: Icon,
title,
description,
action,
className,
}: EmptyStateProps) {
return (
<div
className={cn(
"flex flex-col items-center justify-center py-12 text-center",
className
)}
>
<div className="bg-muted rounded-full p-4">
<Icon className="text-muted-foreground h-8 w-8" />
</div>
<h3 className="mt-4 text-lg font-semibold">{title}</h3>
<p className="text-muted-foreground mt-2 max-w-sm text-sm">
{description}
</p>
{action && <div className="mt-6">{action}</div>}
</div>
)
}import { FolderOpen, Plus } from "lucide-react"
import { getUserMessage } from "@/lib/error-messages"
import { useProjects } from "@/hooks/use-projects"
import { Button } from "@/components/ui/button"
import { EmptyState } from "@/components/empty-state"
import { ErrorMessage } from "@/components/error-message"
import { ProjectCard } from "@/components/project-card"
import { ProjectGridSkeleton } from "@/components/skeletons/project-card-skeleton"
export function ProjectsList() {
const { data: projects, isLoading, error, refetch } = useProjects()
// Loading state
if (isLoading) {
return <ProjectGridSkeleton count={6} />
}
// Error state
if (error) {
return (
<ErrorMessage
title="Failed to load projects"
message={getUserMessage(error)}
onRetry={refetch}
/>
)
}
// Empty state
if (!projects?.length) {
return (
<EmptyState
icon={FolderOpen}
title="No projects yet"
description="Get started by creating your first project."
action={
<Button>
<Plus className="mr-2 h-4 w-4" />
Create Project
</Button>
}
/>
)
}
// Success state
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
)
}// CORRECT - All icon buttons have accessible names
// Using aria-label (preferred)
<Button variant="ghost" size="icon" aria-label="Edit project">
<Pencil className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" aria-label="Delete project">
<Trash2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" aria-label="View notifications">
<Bell className="h-4 w-4" />
</Button>
// Using sr-only span (alternative)
<Button variant="ghost" size="icon">
<Settings className="h-4 w-4" />
<span className="sr-only">Open settings</span>
</Button>
// With dynamic context
<Button
variant="ghost"
size="icon"
aria-label={`Delete "${project.name}"`}
>
<Trash2 className="h-4 w-4" />
</Button>
// Toggle button with state
<Button
variant="ghost"
size="icon"
aria-label={isExpanded ? 'Collapse section' : 'Expand section'}
aria-expanded={isExpanded}
>
{isExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>// CORRECT - Accessible form with proper labels and error handling
<FormField
control={form.control}
name="email"
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>
Email Address
<span className="text-destructive" aria-hidden="true">
{" "}
*
</span>
</FormLabel>
<FormControl>
<Input
type="email"
placeholder="you@example.com"
aria-required="true"
aria-invalid={!!fieldState.error}
aria-describedby={
fieldState.error
? `${field.name}-error`
: `${field.name}-description`
}
{...field}
/>
</FormControl>
<FormDescription id={`${field.name}-description`}>
We'll never share your email with anyone.
</FormDescription>
<FormMessage id={`${field.name}-error`} />
</FormItem>
)}
/>// CORRECT - Keyboard accessible dropdown menu
function ActionsMenu({ project }: { readonly project: Project }) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label={`Actions for ${project.name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => handleEdit(project)}>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => handleDuplicate(project)}>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={() => handleDelete(project)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}import { useQueryClient } from "@tanstack/react-query"
import { Calendar, MoreHorizontal } from "lucide-react"
import { Link } from "react-router"
import { projectsApi, type ProjectResponse } from "@/lib/api/projects-api"
import { formatDate } from "@/lib/date-utils"
import { cn } from "@/lib/utils"
import { queryKeys } from "@/hooks/query-keys"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
// Props are readonly
type ProjectCardProps = {
readonly project: ProjectResponse
readonly className?: string
}
// Sub-component defined at module level (not nested)
function ProjectStatusBadge({ status }: { readonly status: string }) {
const variants: Record<string, "default" | "secondary" | "destructive"> = {
active: "default",
draft: "secondary",
archived: "destructive",
}
return <Badge variant={variants[status] ?? "secondary"}>{status}</Badge>
}
// Named export (not default)
export function ProjectCard({ project, className }: ProjectCardProps) {
const queryClient = useQueryClient()
// Prefetch on hover
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: queryKeys.projects.detail(project.id),
queryFn: () => projectsApi.getById(project.id),
})
}
return (
<Card
className={cn("transition-shadow hover:shadow-md", className)}
onMouseEnter={handleMouseEnter}
>
<CardHeader className="flex flex-row items-start justify-between space-y-0">
<div className="space-y-1">
<CardTitle className="line-clamp-1">
<Link
to={`/projects/${project.id}`}
className="focus:ring-ring hover:underline focus:ring-2 focus:outline-none"
>
{project.name}
</Link>
</CardTitle>
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<Calendar className="h-3 w-3" aria-hidden="true" />
<span>{formatDate(project.createdAt)}</span>
</div>
</div>
<Button
variant="ghost"
size="icon"
aria-label={`Actions for ${project.name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</CardHeader>
<CardContent>
{project.description && (
<p className="text-muted-foreground line-clamp-2 text-sm">
{project.description}
</p>
)}
<div className="mt-4 flex gap-2">
<ProjectStatusBadge status={project.status} />
</div>
</CardContent>
</Card>
)
}import { lazy, Suspense } from "react"
import { Skeleton } from "@/components/ui/skeleton"
// Lazy load the heavy editor component
const PlateEditor = lazy(() => import("@/components/plate-editor"))
type RichTextEditorProps = {
readonly value: string
readonly onChange: (value: string) => void
readonly placeholder?: string
}
export function RichTextEditor(props: RichTextEditorProps) {
return (
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
<PlateEditor {...props} />
</Suspense>
)
}import { lazy, Suspense } from "react"
import { PageSkeleton } from "@/components/skeletons/page-skeleton"
const AnalyticsDashboard = lazy(
() => import("@/components/analytics-dashboard")
)
export function AnalyticsPage() {
return (
<Suspense fallback={<PageSkeleton />}>
<AnalyticsDashboard />
</Suspense>
)
}// BAD - Namespace import for large module
import * as dateFns from "date-fns"
// GOOD - Specific imports
import { differenceInDays, format, parseISO } from "date-fns"
// BAD - Spread in loop accumulator
const result = items.reduce(
(acc, item) => ({
...acc,
[item.id]: item,
}),
{}
)
// GOOD - Direct mutation in accumulator
const result = items.reduce(
(acc, item) => {
acc[item.id] = item
return acc
},
{} as Record<string, Item>
)
// BAD - Creating regex in loop
items.filter((item) => new RegExp(searchTerm, "i").test(item.name))
// GOOD - Regex at top level
const searchRegex = new RegExp(searchTerm, "i")
items.filter((item) => searchRegex.test(item.name))