Cursor rule
.cursor/rules/frontend-react.mdcComplete React component patterns, hooks, and state management with shadcn/ui for POS System
Cursor rules
Quality
69/100
Scores the file, not the repository.Length
2,878 words
35 headings · 19 code blocksRepository
118
— · pushed 339 days agoLast changed
3 days ago
First indexed 3 days ago.123456# ⚛️ React Component Patterns & Best Practices78## 🏗️ Component Architecture910### shadcn/ui Base Components Pattern11```typescript12// Follow the established shadcn/ui pattern from components/ui/13import * as React from 'react'14import { cn } from '@/lib/utils'15import { cva, type VariantProps } from 'class-variance-authority'1617// ✅ CORRECT: Use cva for variant-based styling18const buttonVariants = cva(19 "inline-flex items-center justify-center rounded-md font-medium transition-colors",20 {21 variants: {22 variant: {23 default: "bg-primary text-primary-foreground hover:bg-primary/90",24 outline: "border border-input bg-background hover:bg-accent",25 ghost: "hover:bg-accent hover:text-accent-foreground",26 },27 size: {28 default: "h-10 px-4 py-2",29 sm: "h-9 px-3",30 lg: "h-11 px-8",31 },32 },33 defaultVariants: {34 variant: "default",35 size: "default",36 },37 }38)3940interface ButtonProps41 extends React.ButtonHTMLAttributes<HTMLButtonElement>,42 VariantProps<typeof buttonVariants> {43 asChild?: boolean44}4546// ✅ CORRECT: Always use forwardRef for reusable components47const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(48 ({ className, variant, size, asChild = false, ...props }, ref) => {49 return (50 <button51 ref={ref}52 className={cn(buttonVariants({ variant, size, className }))}53 {...props}54 />55 )56 }57)58Button.displayName = "Button"59```6061### Business Component Pattern62```typescript63// POS-specific business components64import { useState, useCallback } from 'react'65import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'66import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'67import { Button } from '@/components/ui/button'68import { Badge } from '@/components/ui/badge'69import apiClient from '@/api/client'70import type { Product, CartItem } from '@/types'7172interface ProductCardProps {73 product: Product74 onSelect: (product: Product) => void75 isSelected?: boolean76 isInCart?: boolean77 cartQuantity?: number78 className?: string79}8081// ✅ CORRECT: Typed component with proper props interface82export const ProductCard: React.FC<ProductCardProps> = ({83 product,84 onSelect,85 isSelected = false,86 isInCart = false,87 cartQuantity = 0,88 className89}) => {90 // ✅ CORRECT: Use useCallback for event handlers91 const handleSelect = useCallback(() => {92 onSelect(product)93 }, [product, onSelect])9495 // ✅ CORRECT: Early returns for loading/error states96 if (!product.is_available) {97 return (98 <Card className={cn("opacity-50", className)}>99 <CardContent className="p-4 text-center">100 <p className="text-muted-foreground">Unavailable</p>101 </CardContent>102 </Card>103 )104 }105106 return (107 <Card108 className={cn(109 "cursor-pointer transition-all hover:shadow-md",110 isSelected && "ring-2 ring-primary",111 className112 )}113 onClick={handleSelect}114 >115 <CardHeader className="pb-2">116 <div className="flex items-center justify-between">117 <CardTitle className="text-lg">{product.name}</CardTitle>118 {isInCart && (119 <Badge variant="secondary">{cartQuantity}</Badge>120 )}121 </div>122 </CardHeader>123 <CardContent>124 <p className="text-sm text-muted-foreground mb-2">125 {product.description}126 </p>127 <p className="text-xl font-bold">${product.price.toFixed(2)}</p>128 </CardContent>129 </Card>130 )131}132```133134## 🎣 Custom Hooks Patterns135136### Data Fetching Hook137```typescript138// Custom hook for API data fetching139import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'140import { toastHelpers } from '@/lib/toast-helpers'141import apiClient from '@/api/client'142import type { Product, CreateProductRequest } from '@/types'143144export const useProducts = (categoryId?: string) => {145 return useQuery({146 queryKey: ['products', categoryId],147 queryFn: () => apiClient.getProducts({ category_id: categoryId }),148 select: (data) => data.data || [], // Transform response149 staleTime: 5 * 60 * 1000, // 5 minutes150 })151}152153export const useCreateProduct = () => {154 const queryClient = useQueryClient()155156 return useMutation({157 mutationFn: (product: CreateProductRequest) =>158 apiClient.createProduct(product),159 onSuccess: (data) => {160 // Invalidate and refetch161 queryClient.invalidateQueries({ queryKey: ['products'] })162 toastHelpers.success('Product created successfully!')163 },164 onError: (error: any) => {165 toastHelpers.error(`Failed to create product: ${error.message}`)166 },167 })168}169```170171### Business Logic Hook (Shopping Cart)172```typescript173// Custom hook for cart management174import { useState, useCallback, useMemo } from 'react'175import type { Product, CartItem } from '@/types'176177export const useCart = () => {178 const [items, setItems] = useState<CartItem[]>([])179180 // ✅ CORRECT: Memoized calculations181 const total = useMemo(() => {182 return items.reduce((sum, item) => sum + (item.product.price * item.quantity), 0)183 }, [items])184185 const itemCount = useMemo(() => {186 return items.reduce((count, item) => count + item.quantity, 0)187 }, [items])188189 // ✅ CORRECT: Optimized with useCallback190 const addItem = useCallback((product: Product, quantity = 1) => {191 setItems(prevItems => {192 const existingItem = prevItems.find(item => item.product.id === product.id)193194 if (existingItem) {195 return prevItems.map(item =>196 item.product.id === product.id197 ? { ...item, quantity: item.quantity + quantity }198 : item199 )200 }201202 return [...prevItems, { product, quantity, subtotal: product.price * quantity }]203 })204 }, [])205206 const updateQuantity = useCallback((productId: string, quantity: number) => {207 if (quantity <= 0) {208 removeItem(productId)209 return210 }211212 setItems(prevItems =>213 prevItems.map(item =>214 item.product.id === productId215 ? { ...item, quantity, subtotal: item.product.price * quantity }216 : item217 )218 )219 }, [])220221 const removeItem = useCallback((productId: string) => {222 setItems(prevItems => prevItems.filter(item => item.product.id !== productId))223 }, [])224225 const clearCart = useCallback(() => {226 setItems([])227 }, [])228229 return {230 items,231 total,232 itemCount,233 addItem,234 updateQuantity,235 removeItem,236 clearCart,237 isEmpty: items.length === 0,238 }239}240```241242## 📝 Form Handling Patterns243244### React Hook Form + Zod Integration245```typescript246// Form with validation using React Hook Form + Zod247import { useForm } from 'react-hook-form'248import { zodResolver } from '@hookform/resolvers/zod'249import { z } from 'zod'250import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'251import { Input } from '@/components/ui/input'252import { Button } from '@/components/ui/button'253import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'254255// ✅ CORRECT: Define Zod schema with proper validation256const productSchema = z.object({257 name: z.string().min(1, "Product name is required").max(100),258 price: z.number().min(0.01, "Price must be greater than 0"),259 category_id: z.string().min(1, "Category is required"),260 description: z.string().optional(),261 is_available: z.boolean().default(true),262})263264type ProductFormData = z.infer<typeof productSchema>265266interface ProductFormProps {267 onSubmit: (data: ProductFormData) => void268 defaultValues?: Partial<ProductFormData>269 isLoading?: boolean270}271272export const ProductForm: React.FC<ProductFormProps> = ({273 onSubmit,274 defaultValues,275 isLoading = false276}) => {277 const form = useForm<ProductFormData>({278 resolver: zodResolver(productSchema),279 defaultValues: {280 name: '',281 price: 0,282 category_id: '',283 description: '',284 is_available: true,285 ...defaultValues,286 },287 })288289 return (290 <Form {...form}>291 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">292 <FormField293 control={form.control}294 name="name"295 render={({ field }) => (296 <FormItem>297 <FormLabel>Product Name</FormLabel>298 <FormControl>299 <Input placeholder="Enter product name" {...field} />300 </FormControl>301 <FormMessage />302 </FormItem>303 )}304 />305306 <FormField307 control={form.control}308 name="price"309 render={({ field }) => (310 <FormItem>311 <FormLabel>Price</FormLabel>312 <FormControl>313 <Input314 type="number"315 step="0.01"316 placeholder="0.00"317 {...field}318 onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}319 />320 </FormControl>321 <FormMessage />322 </FormItem>323 )}324 />325326 <FormField327 control={form.control}328 name="category_id"329 render={({ field }) => (330 <FormItem>331 <FormLabel>Category</FormLabel>332 <Select onValueChange={field.onChange} defaultValue={field.value}>333 <FormControl>334 <SelectTrigger>335 <SelectValue placeholder="Select a category" />336 </SelectTrigger>337 </FormControl>338 <SelectContent>339 <SelectItem value="burgers">Burgers</SelectItem>340 <SelectItem value="drinks">Drinks</SelectItem>341 <SelectItem value="sides">Sides</SelectItem>342 </SelectContent>343 </Select>344 <FormMessage />345 </FormItem>346 )}347 />348349 <Button type="submit" disabled={isLoading} className="w-full">350 {isLoading ? 'Creating...' : 'Create Product'}351 </Button>352 </form>353 </Form>354 )355}356```357358### Reusable Form Components359```typescript360// Generic form field wrapper for consistent styling361interface FormFieldWrapperProps<T extends FieldValues> {362 control: Control<T>363 name: FieldPath<T>364 label: string365 description?: string366 required?: boolean367 children: React.ReactNode368}369370export function FormFieldWrapper<T extends FieldValues>({371 control,372 name,373 label,374 description,375 required = false,376 children,377}: FormFieldWrapperProps<T>) {378 return (379 <FormField380 control={control}381 name={name}382 render={() => (383 <FormItem>384 <FormLabel>385 {label}386 {required && <span className="text-destructive ml-1">*</span>}387 </FormLabel>388 <FormControl>{children}</FormControl>389 {description && <FormDescription>{description}</FormDescription>}390 <FormMessage />391 </FormItem>392 )}393 />394 )395}396```397398## 🍳 Enhanced Kitchen Component Patterns399400### As-Ready Service Workflow Components401```typescript402// Enhanced kitchen interface with individual item tracking403import React, { useState, useCallback, useEffect } from 'react'404import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'405import { Button } from '@/components/ui/button'406import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'407import { Badge } from '@/components/ui/badge'408import { CheckCircle } from 'lucide-react'409import { cn } from '@/lib/utils'410411interface EnhancedKitchenOrderProps {412 order: Order413 onItemStatusUpdate: (orderId: string, itemId: string, status: ItemStatus) => void414 onItemServe: (orderId: string, itemId: string) => void415 soundEnabled: boolean416 volume: number417}418419// ✅ CORRECT: Enhanced kitchen order card with as-ready service420export const EnhancedKitchenOrderCard: React.FC<EnhancedKitchenOrderProps> = ({421 order,422 onItemStatusUpdate,423 onItemServe,424 soundEnabled,425 volume426}) => {427 const [checkedItems, setCheckedItems] = useState<Set<string>>(new Set())428429 // Calculate progress with individual item tracking430 const displayItems = order.items || []431 const totalItems = displayItems.length432 const readyItems = checkedItems.size433 const servedItems = displayItems.filter(item => item.status === 'served').length434 const progress = totalItems > 0 ? ((readyItems + servedItems) / totalItems) * 100 : 0435436 // ✅ CORRECT: Individual item status management437 const toggleItem = useCallback((itemId: string) => {438 setCheckedItems(prev => {439 const newSet = new Set(prev)440 if (newSet.has(itemId)) {441 newSet.delete(itemId)442 onItemStatusUpdate(order.id, itemId, 'preparing')443 } else {444 newSet.add(itemId)445 onItemStatusUpdate(order.id, itemId, 'ready')446447 // Play ready sound notification448 if (soundEnabled) {449 playReadySound(volume)450 }451 }452 return newSet453 })454 }, [order.id, onItemStatusUpdate, soundEnabled, volume])455456 // ✅ CORRECT: Individual item serving457 const handleItemServe = useCallback((itemId: string, itemName: string) => {458 onItemServe(order.id, itemId)459460 // Play served sound notification (distinct from ready)461 if (soundEnabled) {462 playServedSound(volume)463 }464465 // Remove from checked items466 setCheckedItems(prev => {467 const newSet = new Set(prev)468 newSet.delete(itemId)469 return newSet470 })471 }, [order.id, onItemServe, soundEnabled, volume])472473 // Sound notification functions474 const playReadySound = (volume: number) => {475 try {476 const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()477 const oscillator = audioContext.createOscillator()478 const gainNode = audioContext.createGain()479480 oscillator.connect(gainNode)481 gainNode.connect(audioContext.destination)482483 oscillator.frequency.setValueAtTime(1200, audioContext.currentTime) // 1200Hz for ready484 gainNode.gain.setValueAtTime(volume * 0.3, audioContext.currentTime)485486 oscillator.start()487 oscillator.stop(audioContext.currentTime + 0.3)488 } catch (error) {489 console.log('Sound notification failed:', error)490 }491 }492493 const playServedSound = (volume: number) => {494 try {495 const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()496 const oscillator = audioContext.createOscillator()497 const gainNode = audioContext.createGain()498499 oscillator.connect(gainNode)500 gainNode.connect(audioContext.destination)501502 oscillator.frequency.setValueAtTime(1400, audioContext.currentTime) // 1400Hz for served503 gainNode.gain.setValueAtTime(volume * 0.2, audioContext.currentTime)504505 oscillator.start()506 oscillator.stop(audioContext.currentTime + 0.2)507 } catch (error) {508 console.log('Sound notification failed:', error)509 }510 }511512 return (513 <Card className="mb-6 border-2 border-blue-200 bg-white shadow-lg">514 <CardHeader className="pb-4">515 <div className="flex items-center justify-between">516 <CardTitle className="text-xl font-bold text-gray-900">517 Order #{order.order_number}518 </CardTitle>519 <Badge variant="outline" className="text-sm">520 {order.order_type === 'dine_in' ? `Table ${order.table?.table_number}` : 'Takeaway'}521 </Badge>522 </div>523524 {/* Enhanced progress tracking */}525 <div className="mt-3">526 <div className="flex items-center justify-between text-sm text-gray-600 mb-2">527 <span>Progress</span>528 <span>{readyItems} ready • {servedItems} served • {totalItems - readyItems - servedItems} cooking ({Math.round(progress)}% complete)</span>529 </div>530 <div className="w-full bg-gray-200 rounded-full h-2">531 <div532 className="bg-green-500 h-2 rounded-full transition-all duration-300"533 style={{ width: `${progress}%` }}534 />535 </div>536 </div>537 </CardHeader>538539 <CardContent>540 <div className="space-y-3">541 {displayItems.map((item, index) => {542 const isServed = item.status === 'served'543 const isReady = checkedItems.has(item.id)544545 return (546 <div key={item.id} className={cn(547 "flex items-start space-x-4 p-4 rounded-lg border-2 transition-colors",548 isServed ? "bg-gray-50 border-gray-300 opacity-75" : "bg-white hover:border-blue-200"549 )}>550 {/* Item checkbox */}551 <button552 onClick={() => !isServed && toggleItem(item.id)}553 disabled={isServed}554 className={cn(555 "w-8 h-8 rounded-lg border-2 flex items-center justify-center transition-all mt-1 flex-shrink-0",556 isServed557 ? "bg-gray-400 border-gray-400 text-white cursor-not-allowed"558 : isReady559 ? "bg-green-500 border-green-500 text-white shadow-lg"560 : "border-gray-300 hover:border-green-400 hover:bg-green-50"561 )}562 >563 {(isReady || isServed) && <CheckCircle className="w-5 h-5" />}564 </button>565566 <div className="flex-1 min-w-0">567 <div className={cn(568 "font-semibold text-lg mb-2",569 isServed ? "line-through text-gray-500" : isReady && "line-through text-muted-foreground"570 )}>571 {item.quantity}x {item.product?.name || `Item ${index + 1}`}572 {isServed && <span className="ml-2 text-xs bg-gray-200 text-gray-700 px-2 py-1 rounded">SERVED</span>}573 </div>574575 {/* Special instructions */}576 {item.special_instructions && (577 <div className="text-sm text-orange-600 bg-orange-50 px-2 py-1 rounded mb-2">578 Note: {item.special_instructions}579 </div>580 )}581582 <div className="flex items-center justify-between mt-2">583 {/* Status indicator */}584 <div className={cn(585 "text-xs font-medium px-2 py-1 rounded-full",586 isServed587 ? "bg-gray-100 text-gray-600"588 : isReady589 ? "bg-green-100 text-green-800"590 : "bg-orange-100 text-orange-800"591 )}>592 {isServed ? '🍽️ Served' : isReady ? '✅ Ready' : '🍳 Cooking'}593 </div>594595 {/* Individual serve button */}596 {isReady && !isServed && (597 <Button598 size="sm"599 variant="outline"600 className="h-6 px-2 text-xs bg-blue-50 hover:bg-blue-100 border-blue-300"601 onClick={(e) => {602 e.stopPropagation()603 handleItemServe(item.id, item.product?.name || 'Item')604 }}605 >606 🍽️ Serve Now607 </Button>608 )}609 </div>610 </div>611 </div>612 )613 })}614 </div>615 </CardContent>616 </Card>617 )618}619```620621### Sound Notification System622```typescript623// Sound notification service for kitchen operations624class KitchenSoundService {625 private audioContext: AudioContext | null = null626 private enabled: boolean = true627 private volume: number = 0.5628629 async initialize(): Promise<void> {630 try {631 this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()632 } catch (error) {633 console.warn('Audio context not available:', error)634 }635 }636637 // ✅ CORRECT: Different sounds for different kitchen events638 playNewOrderSound(): void {639 this.playTone(800, 0.5, this.volume * 0.4) // 800Hz for new orders640 }641642 playAllReadySound(): void {643 this.playTone(1200, 0.3, this.volume * 0.3) // 1200Hz for all items ready644 }645646 playItemServedSound(): void {647 this.playTone(1400, 0.2, this.volume * 0.2) // 1400Hz for item served648 }649650 private playTone(frequency: number, duration: number, volume: number): void {651 if (!this.enabled || !this.audioContext) return652653 try {654 const oscillator = this.audioContext.createOscillator()655 const gainNode = this.audioContext.createGain()656657 oscillator.connect(gainNode)658 gainNode.connect(this.audioContext.destination)659660 oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime)661 gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime)662663 oscillator.start()664 oscillator.stop(this.audioContext.currentTime + duration)665 } catch (error) {666 console.log('Sound playback failed:', error)667 }668 }669670 setEnabled(enabled: boolean): void {671 this.enabled = enabled672 }673674 setVolume(volume: number): void {675 this.volume = Math.max(0, Math.min(1, volume))676 }677}678```679680## 🎭 Role-Based Component Patterns681682### Role-Specific Interface Components683```typescript684// Role-based component rendering685import { User } from '@/types'686687interface RoleBasedLayoutProps {688 user: User689}690691// ✅ CORRECT: Role-based component switching692export const RoleBasedLayout: React.FC<RoleBasedLayoutProps> = ({ user }) => {693 // Admin gets access to all interfaces694 if (user.role === 'admin') {695 return <AdminLayout user={user} />696 }697698 // Role-specific interfaces699 switch (user.role) {700 case 'server':701 return <ServerInterface user={user} />702 case 'counter':703 return <CounterInterface user={user} />704 case 'kitchen':705 return <NewEnhancedKitchenLayout user={user} /> // Enhanced with as-ready service706 default:707 return <POSLayout user={user} />708 }709}710711// Role-based feature flags712interface FeatureGateProps {713 allowedRoles: string[]714 userRole: string715 children: React.ReactNode716 fallback?: React.ReactNode717}718719export const FeatureGate: React.FC<FeatureGateProps> = ({720 allowedRoles,721 userRole,722 children,723 fallback = null724}) => {725 if (allowedRoles.includes(userRole)) {726 return <>{children}</>727 }728729 return <>{fallback}</>730}731732// Usage in components733<FeatureGate allowedRoles={['admin', 'manager']} userRole={user.role}>734 <AdminReportsButton />735</FeatureGate>736```737738## 🎨 Styling & UI Patterns739740### Conditional Styling with cn()741```typescript742// ✅ CORRECT: Use cn() utility for conditional classes743import { cn } from '@/lib/utils'744745const OrderCard = ({ order, isSelected, status }) => {746 return (747 <Card className={cn(748 "transition-all duration-200",749 isSelected && "ring-2 ring-primary",750 status === 'urgent' && "border-destructive",751 status === 'completed' && "opacity-75"752 )}>753 {/* Content */}754 </Card>755 )756}757```758759### Status-Based Styling760```typescript761// Status badge utility762const getStatusVariant = (status: string) => {763 const variants = {764 pending: 'secondary',765 confirmed: 'default',766 preparing: 'warning',767 ready: 'success',768 served: 'outline',769 completed: 'success',770 cancelled: 'destructive',771 } as const772773 return variants[status] || 'secondary'774}775776// Usage777<Badge variant={getStatusVariant(order.status)}>778 {order.status}779</Badge>780```781782## ⚡ Performance Optimization Patterns783784### React.memo for Expensive Components785```typescript786// ✅ CORRECT: Memo for components with expensive renders787export const ProductGrid = React.memo<ProductGridProps>(({788 products,789 onProductSelect,790 selectedProducts791}) => {792 return (793 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">794 {products.map(product => (795 <ProductCard796 key={product.id}797 product={product}798 onSelect={onProductSelect}799 isSelected={selectedProducts.includes(product.id)}800 />801 ))}802 </div>803 )804}, (prevProps, nextProps) => {805 // Custom comparison for optimization806 return (807 prevProps.products.length === nextProps.products.length &&808 prevProps.selectedProducts.length === nextProps.selectedProducts.length809 )810})811```812813### Virtualization for Large Lists814```typescript815// For large product catalogs or order lists816import { FixedSizeList as List } from 'react-window'817818const VirtualizedProductList = ({ products, onSelect }) => {819 const Row = ({ index, style }) => (820 <div style={style}>821 <ProductCard822 product={products[index]}823 onSelect={onSelect}824 />825 </div>826 )827828 return (829 <List830 height={600}831 itemCount={products.length}832 itemSize={120}833 width="100%"834 >835 {Row}836 </List>837 )838}839```840841## 🔄 State Management Patterns842843### Optimistic Updates844```typescript845// Optimistic UI updates for better UX846const useOptimisticOrderStatus = () => {847 const queryClient = useQueryClient()848849 return useMutation({850 mutationFn: ({ orderId, status }) =>851 apiClient.updateOrderStatus(orderId, status),852853 onMutate: async ({ orderId, status }) => {854 // Cancel outgoing refetches855 await queryClient.cancelQueries({ queryKey: ['orders'] })856857 // Snapshot previous value858 const previousOrders = queryClient.getQueryData(['orders'])859860 // Optimistically update861 queryClient.setQueryData(['orders'], (old: any) =>862 old?.map((order: any) =>863 order.id === orderId ? { ...order, status } : order864 )865 )866867 return { previousOrders }868 },869870 onError: (err, variables, context) => {871 // Rollback on error872 if (context?.previousOrders) {873 queryClient.setQueryData(['orders'], context.previousOrders)874 }875 toastHelpers.error('Failed to update order status')876 },877878 onSettled: () => {879 // Refetch to ensure consistency880 queryClient.invalidateQueries({ queryKey: ['orders'] })881 },882 })883}884```885886## 🎯 Error Handling Patterns887888### Error Boundaries889```typescript890// Error boundary for catching component errors891class OrderErrorBoundary extends React.Component {892 constructor(props) {893 super(props)894 this.state = { hasError: false, error: null }895 }896897 static getDerivedStateFromError(error) {898 return { hasError: true, error }899 }900901 componentDidCatch(error, errorInfo) {902 console.error('Order component error:', error, errorInfo)903 }904905 render() {906 if (this.state.hasError) {907 return (908 <Card className="p-6 text-center">909 <CardHeader>910 <CardTitle className="text-destructive">Something went wrong</CardTitle>911 </CardHeader>912 <CardContent>913 <p className="text-muted-foreground mb-4">914 We encountered an error while processing your order.915 </p>916 <Button917 onClick={() => this.setState({ hasError: false, error: null })}918 variant="outline"919 >920 Try Again921 </Button>922 </CardContent>923 </Card>924 )925 }926927 return this.props.children928 }929}930```931932### Loading States933```typescript934// Consistent loading patterns935const LoadingStates = {936 Spinner: () => (937 <div className="flex items-center justify-center p-4">938 <Loader2 className="h-6 w-6 animate-spin" />939 </div>940 ),941942 Skeleton: () => (943 <div className="space-y-3">944 <div className="h-4 bg-muted animate-pulse rounded" />945 <div className="h-4 bg-muted animate-pulse rounded w-3/4" />946 </div>947 ),948949 Button: ({ isLoading, children, ...props }) => (950 <Button disabled={isLoading} {...props}>951 {isLoading && <Loader2 className="h-4 w-4 animate-spin mr-2" />}952 {children}953 </Button>954 ),955}956```957958## 🛠️ Development Patterns959960### Component Organization961```typescript962// ✅ CORRECT: Well-organized component file963// ProductCard/index.tsx964export { ProductCard } from './ProductCard'965export type { ProductCardProps } from './ProductCard'966967// ProductCard/ProductCard.tsx968import { memo, useCallback } from 'react'969// ... imports970971interface ProductCardProps {972 // ... props973}974975export const ProductCard: React.FC<ProductCardProps> = memo(({976 // ... implementation977}))978979// ProductCard/ProductCard.stories.tsx (if using Storybook)980export default {981 title: 'Components/ProductCard',982 component: ProductCard,983}984985// ProductCard/ProductCard.test.tsx986describe('ProductCard', () => {987 // ... tests988})989```990991### Type Safety Best Practices992```typescript993// ✅ CORRECT: Strict typing throughout994import type { Product, User, Order } from '@/types'995996// Generic component with proper constraints997interface DataTableProps<T> {998 data: T[]999 columns: Array<{1000 key: keyof T1001 header: string1002 render?: (value: T[keyof T], row: T) => React.ReactNode1003 }>1004 onRowClick?: (row: T) => void1005}10061007export function DataTable<T extends Record<string, any>>({1008 data,1009 columns,1010 onRowClick1011}: DataTableProps<T>) {1012 // Implementation with full type safety1013}1014```10151016## 🚀 Development Commands & Tools10171018### Essential Commands1019```bash1020# Component development1021npm run dev # Start development server1022npm run build # Build for production1023npm run type-check # TypeScript checking10241025# Quality assurance1026npm run lint # ESLint checking1027npm run format # Prettier formatting1028npm run test # Run component tests10291030# Component generation (if using generator)1031npm run generate:component ProductCard1032```10331034### Recommended VS Code Extensions1035- **ES7+ React/Redux/React-Native snippets**1036- **Auto Rename Tag**1037- **Bracket Pair Colorizer**1038- **Tailwind CSS IntelliSense**1039- **TypeScript Importer**1040- **React Hook Form DevTools**
Also in madebyaris/poinf-of-sales
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| madebyaris/poinf-of-sales.cursor/rules/admin-interface-patterns.mdc · 118 | Cursor rules | stylearchsecurityapi+2 | 62/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/api-patterns.mdc · 118 | Cursor rules | lint-formatstylesecuritydatabase+3 | 62/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/authentication-and-security-patterns.mdc · 118 | Cursor rules | setupteststylesecurity+4 | 81/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/backend-golang.mdc · 118 | Cursor rules | testlint-formatstylearch+5 | 69/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/business-logic-patterns.mdc · 118 | Cursor rules | teststyledatabaseperformance+1 | 50/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/database-patterns.mdc · 118 | Cursor rules | stylearchtypessecurity+2 | 62/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/development-workflow.mdc · 118 | Cursor rules | setupbuildteststyle+3 | 86/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/docker-deployment.mdc · 118 | Cursor rules | setupbuildteststyle+8 | 77/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/makefile-scripting.mdc · 118 | Cursor rules | setuplint-formatstylearch+2 | 81/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/performance-optimization-patterns.mdc · 118 | Cursor rules | buildteststyledatabase+3 | 66/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/project-architecture.mdc · 118 | Cursor rules | setupteststylearch+6 | 78/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/react-native-mobile-patterns.mdc · 118 | Cursor rules | buildstylearchui+2 | 74/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/role-based-access-patterns.mdc · 118 | Cursor rules | styletypessecuritydatabase+2 | 58/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/tech-debt-prevention.mdc · 118 | Cursor rules | styletesting-strategyui | 50/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/testing-patterns.mdc · 118 | Cursor rules | setupteststylearch+4 | 74/100 | 3 days ago | |
| madebyaris/poinf-of-sales.cursor/rules/user-journey-optimization.mdc · 118 | Cursor rules | styleperformanceagent-behaviour | 50/100 | 3 days ago |
Diff against .cursor/rules/admin-interface-patterns.mdc Diff against .cursor/rules/api-patterns.mdc Diff against .cursor/rules/authentication-and-security-patterns.mdc Diff against .cursor/rules/backend-golang.mdc Diff against .cursor/rules/business-logic-patterns.mdc Diff against .cursor/rules/database-patterns.mdc Diff against .cursor/rules/development-workflow.mdc Diff against .cursor/rules/docker-deployment.mdc Diff against .cursor/rules/makefile-scripting.mdc Diff against .cursor/rules/performance-optimization-patterns.mdc Diff against .cursor/rules/project-architecture.mdc Diff against .cursor/rules/react-native-mobile-patterns.mdc Diff against .cursor/rules/role-based-access-patterns.mdc Diff against .cursor/rules/tech-debt-prevention.mdc Diff against .cursor/rules/testing-patterns.mdc Diff against .cursor/rules/user-journey-optimization.mdc
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| hiromaily/go-crypto-wallet.cursor/rules/typescript.mdc · 126 | Cursor rules | setupbuildtestlint-format+6 | 100/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/10-hermes-api.mdc · 45 | Cursor rules | testlint-formatstylearch+5 | 100/100 | 3 days ago | |
| markstev/mark-starter.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+6 | 99/100 | 3 days ago | |
| Allymahmoud/case-intake-platform.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| dodgecfr/combatfilms-webapp.cursor/rules/frontend.mdc · 0 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| deifos/clipmira-subtitles.cursor/rules/frontend.mdc · 1 | Cursor rules | setuptestlint-formatstyle+7 | 99/100 | 3 days ago | |
| langflow-ai/langflow.cursor/rules/docs_development.mdc · 153k | Cursor rules | setupbuildtestlint-format+7 | 97/100 | 3 days ago | |
| TechSquidTV/Hermes.cursor/rules/20-hermes-api-tests.mdc · 45 | Cursor rules | teststyletesting-strategysecurity+3 | 97/100 | 3 days ago |
