Component Architecture
Quality Hub GINEZ uses a component-based architecture with React and shadcn/ui for a consistent, accessible, and maintainable UI.Component Hierarchy
Root Layout (app/layout.tsx)
│
├── Theme Provider
│ └── Dark/light mode context
│
├── Auth Provider
│ └── User authentication context
│
├── App Shell
│ ├── Sidebar Navigation
│ ├── Top Bar
│ └── Main Content Area
│ └── Page Components
│ ├── Feature Components
│ └── UI Components
│
└── Toast Notifications (Sonner)
Component Categories
1. Layout Components
Provide structure and consistent layout across pages.AppShell
Main application shell with navigation:// components/AppShell.tsx
export function AppShell({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const { user, profile } = useAuth()
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
return (
<div className="flex min-h-screen">
<Sidebar collapsed={isSidebarCollapsed} />
<MainContent>{children}</MainContent>
</div>
)
}
Breadcrumbs
Breadcrumb navigation:// components/Breadcrumbs.tsx
export function Breadcrumbs() {
const pathname = usePathname()
const segments = pathname.split('/').filter(Boolean)
return (
<nav className="flex items-center space-x-2 text-sm">
{segments.map((segment, index) => (
<Fragment key={segment}>
<Link href={`/${segments.slice(0, index + 1).join('/')}`}>
{formatSegment(segment)}
</Link>
{index < segments.length - 1 && <ChevronRight />}
</Fragment>
))}
</nav>
)
}
2. Feature Components
Domain-specific components for business logic.DataTable
Reusable data table with sorting, filtering, and pagination:// components/DataTable.tsx
import { useReactTable, getCoreRowModel } from '@tanstack/react-table'
export function DataTable<T>({
data,
columns,
onRowClick
}: DataTableProps<T>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map(headerGroup => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(header => (
<TableHead key={header.id}>
{flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map(row => (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map(cell => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}
ModuleCard
Dashboard module cards:// components/ModuleCard.tsx
export function ModuleCard({
title,
description,
icon: Icon,
href,
color
}: ModuleCardProps) {
return (
<Link href={href}>
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
<CardHeader>
<div className={cn("p-3 rounded-lg w-fit", color)}>
<Icon className="h-6 w-6" />
</div>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
</Card>
</Link>
)
}
DashboardBanner
Carousel banner component:// components/DashboardBanner.tsx
import { Carousel, CarouselContent, CarouselItem } from '@/components/ui/carousel'
import Autoplay from 'embla-carousel-autoplay'
export function DashboardBanner({ slides }: { slides: Slide[] }) {
const plugin = useRef(Autoplay({ delay: 5000, stopOnInteraction: true }))
return (
<Carousel plugins={[plugin.current]} className="w-full">
<CarouselContent>
{slides.map((slide, index) => (
<CarouselItem key={index}>
<div className="relative h-64 rounded-lg overflow-hidden">
<img src={slide.image} alt={slide.title} />
<div className="absolute inset-0 bg-gradient-to-t from-black/60">
<h3 className="text-white text-2xl font-bold">{slide.title}</h3>
<p className="text-white/90">{slide.description}</p>
</div>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
)
}
3. Form Components
Form inputs with validation and error handling.Controlled Input Pattern
export function ProductForm({ onSubmit }: ProductFormProps) {
const [formData, setFormData] = useState({
codigo_producto: '',
tamano_lote: '',
ph: ''
})
const [errors, setErrors] = useState<Record<string, string>>({})
const handleSubmit = async (e: FormEvent) => {
e.preventDefault()
// Validate with Zod
const result = validateForm(BitacoraSchema, formData)
if (!result.success) {
setErrors(result.errors)
return
}
// Submit
await onSubmit(result.data)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="codigo_producto">Código de Producto</Label>
<Select
value={formData.codigo_producto}
onValueChange={(value) =>
setFormData(prev => ({ ...prev, codigo_producto: value }))
}
>
<SelectTrigger id="codigo_producto">
<SelectValue placeholder="Selecciona un producto" />
</SelectTrigger>
<SelectContent>
{products.map(product => (
<SelectItem key={product.code} value={product.code}>
{product.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.codigo_producto && (
<p className="text-sm text-red-500 mt-1">{errors.codigo_producto}</p>
)}
</div>
<div>
<Label htmlFor="tamano_lote">Tamaño de Lote</Label>
<Input
id="tamano_lote"
type="number"
value={formData.tamano_lote}
onChange={(e) =>
setFormData(prev => ({ ...prev, tamano_lote: e.target.value }))
}
/>
{errors.tamano_lote && (
<p className="text-sm text-red-500 mt-1">{errors.tamano_lote}</p>
)}
</div>
<Button type="submit">Guardar</Button>
</form>
)
}
4. UI Components (shadcn/ui)
Base UI components from shadcn/ui, fully customizable.Button
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3",
lg: "h-11 px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
export { Button, buttonVariants }
<Button variant="default">Save</Button>
<Button variant="outline" size="sm">Cancel</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost" size="icon"><Icon /></Button>
Dialog (Modal)
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
export function ConfirmDialog({ open, onOpenChange, onConfirm }) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>¿Estás seguro?</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p>Esta acción no se puede deshacer.</p>
<div className="flex gap-2 justify-end">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancelar
</Button>
<Button variant="destructive" onClick={onConfirm}>
Confirmar
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}
Select
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
<Select value={value} onValueChange={setValue}>
<SelectTrigger>
<SelectValue placeholder="Selecciona una opción" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Opción 1</SelectItem>
<SelectItem value="option2">Opción 2</SelectItem>
</SelectContent>
</Select>
5. Chart Components
Data visualization with Recharts.Bar Chart Example
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
export function ConformityChart({ data }: { data: ChartData[] }) {
return (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="conforme" fill="#22c55e" name="Conforme" />
<Bar dataKey="semiConforme" fill="#eab308" name="Semi-Conforme" />
<Bar dataKey="noConforme" fill="#ef4444" name="No Conforme" />
</BarChart>
</ResponsiveContainer>
)
}
Control Chart with Reference Lines
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine } from 'recharts'
export function ControlChart({ data, specMin, specMax }: ControlChartProps) {
const warnMin = specMin * 0.95
const warnMax = specMax * 1.05
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="lote" />
<YAxis domain={[warnMin - 1, warnMax + 1]} />
<Tooltip />
{/* Red lines (specification) */}
<ReferenceLine y={specMin} stroke="#ef4444" strokeWidth={2} label="Min Spec" />
<ReferenceLine y={specMax} stroke="#ef4444" strokeWidth={2} label="Max Spec" />
{/* Yellow lines (tolerance) */}
<ReferenceLine y={warnMin} stroke="#eab308" strokeDasharray="3 3" label="-5%" />
<ReferenceLine y={warnMax} stroke="#eab308" strokeDasharray="3 3" label="+5%" />
<Line type="monotone" dataKey="solidos" stroke="#3b82f6" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
)
}
Component Patterns
Composition Pattern
Build complex components from simpler ones:// Card composition
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>
{/* Content */}
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
Compound Component Pattern
Components that work together:// Tabs
<Tabs defaultValue="quality">
<TabsList>
<TabsTrigger value="quality">Calidad</TabsTrigger>
<TabsTrigger value="commercial">Comercial</TabsTrigger>
</TabsList>
<TabsContent value="quality">
<QualityReport />
</TabsContent>
<TabsContent value="commercial">
<CommercialReport />
</TabsContent>
</Tabs>
Render Props Pattern
For flexible rendering:export function DataFetcher<T>({
endpoint,
children
}: {
endpoint: string
children: (data: T[], loading: boolean, error: Error | null) => React.ReactNode
}) {
const [data, setData] = useState<T[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetch(endpoint)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [endpoint])
return <>{children(data, loading, error)}</>
}
// Usage
<DataFetcher endpoint="/api/products">
{(products, loading, error) => {
if (loading) return <Spinner />
if (error) return <Error message={error.message} />
return <ProductList products={products} />
}}
</DataFetcher>
Styling Approach
Tailwind CSS Utility Classes
<div className="flex items-center justify-between p-4 bg-white dark:bg-slate-900 rounded-lg shadow-sm">
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Title</h2>
<Button size="sm">Action</Button>
</div>
Conditional Classes with cn Utility
import { cn } from '@/lib/utils'
<div className={cn(
"base-classes",
isActive && "active-classes",
variant === "primary" && "primary-classes",
className // Allow external classes
)}>
Content
</div>
Class Variance Authority (CVA)
For component variants:import { cva } from "class-variance-authority"
const cardVariants = cva(
"rounded-lg border p-4", // Base classes
{
variants: {
variant: {
default: "bg-white border-slate-200",
success: "bg-green-50 border-green-200",
warning: "bg-yellow-50 border-yellow-200",
danger: "bg-red-50 border-red-200",
},
size: {
sm: "p-2 text-sm",
md: "p-4",
lg: "p-6 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
)
export function InfoCard({ variant, size, children }) {
return (
<div className={cardVariants({ variant, size })}>
{children}
</div>
)
}
Dark Mode Support
All components support dark mode:// Light and dark variants
<div className="bg-white dark:bg-slate-900 text-slate-900 dark:text-white">
Content
</div>
// Theme toggle
const { theme, setTheme } = useTheme()
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? <Sun /> : <Moon />}
</button>
Accessibility
ARIA Labels
<button aria-label="Close dialog" onClick={onClose}>
<X className="h-4 w-4" />
</button>
Keyboard Navigation
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'Enter') onSubmit()
}
<div onKeyDown={handleKeyDown} tabIndex={0}>
Content
</div>
Screen Reader Support
All shadcn/ui components have built-in ARIA support.Performance Best Practices
Memoization
const MemoizedChart = React.memo(ControlChart)
const expensiveData = useMemo(() => {
return data.map(item => analyzeConformity(item))
}, [data])
Lazy Loading
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <Skeleton className="h-64 w-full" />,
ssr: false
})
