--- name: frontend-component description: Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs). --- # Frontend Component Skill **Purpose**: Guidance for creating Next.js components following server/client patterns and existing component structures. ## Overview Next.js 16+ uses App Router with Server Components by default. Client Components are only used when interactivity is needed (hooks, event handlers, browser APIs). ## Server vs Client Components ### Server Components (Default) **When to Use**: - Pages and layouts - Static content - Data fetching from API (when possible) - SEO-optimized content **Pattern**: ```typescript // No "use client" directive import { Metadata } from "next"; export const metadata: Metadata = { title: "Page Title", }; export default function PageComponent() { return
Static content
; } ``` **Example**: `frontend/app/layout.tsx`, `frontend/app/page.tsx` ### Client Components (When Needed) **When to Use**: - Interactive elements (buttons, forms, inputs) - Event handlers (onClick, onChange, etc.) - React hooks (useState, useEffect, useRouter, etc.) - Browser APIs (localStorage, window, document, etc.) - Real-time updates - Drag and drop functionality **Pattern**: ```typescript "use client"; // MUST be first line import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; interface ComponentProps { prop1: string; prop2?: number; } export default function ComponentName({ prop1, prop2 }: ComponentProps) { const router = useRouter(); const [state, setState] = useState(""); useEffect(() => { // Side effects }, []); return
{/* Component JSX */}
; } ``` **Example**: `frontend/components/ProtectedRoute.tsx`, `frontend/app/signup/page.tsx` ## Component Structure Template ```typescript "use client"; // Only if client component /** * Component Name * * Brief description of what this component does */ import { useState, useEffect } from "react"; import { ComponentType } from "@/types"; import { cn } from "@/lib/utils"; interface ComponentProps { prop1: string; prop2?: number; className?: string; } export default function ComponentName({ prop1, prop2, className }: ComponentProps) { // State const [state, setState] = useState(""); // Effects useEffect(() => { // Side effects }, []); // Handlers const handleClick = () => { // Handler logic }; // Render return (
{/* Component content */}
); } ``` ## Specific Component Patterns ### 1. ProtectedRoute Pattern **From**: `frontend/components/ProtectedRoute.tsx` ```typescript "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { isAuthenticated } from "@/lib/auth"; import LoadingSpinner from "./LoadingSpinner"; interface ProtectedRouteProps { children: React.ReactNode; } export default function ProtectedRoute({ children }: ProtectedRouteProps) { const router = useRouter(); const [isAuthorized, setIsAuthorized] = useState(false); const [isChecking, setIsChecking] = useState(true); useEffect(() => { async function checkAuth() { try { const authenticated = await isAuthenticated(); if (!authenticated) { const currentPath = window.location.pathname; if (currentPath !== "/signin") { sessionStorage.setItem("redirectAfterLogin", currentPath); } router.push("/signin"); } else { setIsAuthorized(true); } } catch (error) { console.error("Auth check failed:", error); router.push("/signin"); } finally { setIsChecking(false); } } checkAuth(); }, [router]); if (isChecking) { return (
); } if (!isAuthorized) { return null; } return <>{children}; } ``` **Pattern**: - Check authentication on mount - Show loading spinner during check - Store intended destination in sessionStorage - Redirect to `/signin` if not authenticated - Only render children if authorized ### 2. LoadingSpinner Pattern **From**: `frontend/components/LoadingSpinner.tsx` ```typescript interface LoadingSpinnerProps { size?: "small" | "medium" | "large"; color?: string; label?: string; } export default function LoadingSpinner({ size = "medium", color = "blue", label = "Loading...", }: LoadingSpinnerProps) { const sizeClasses = { small: "w-4 h-4 border-2", medium: "w-8 h-8 border-3", large: "w-12 h-12 border-4", }; const colorClasses = { blue: "border-blue-600 border-t-transparent", gray: "border-gray-600 border-t-transparent", white: "border-white border-t-transparent", }; const spinnerClass = `${sizeClasses[size]} ${ colorClasses[color as keyof typeof colorClasses] || colorClasses.blue } rounded-full animate-spin`; return (
{label}
); } ``` **Pattern**: - Multiple sizes (small, medium, large) - Multiple colors (blue, gray, white) - Accessibility labels (`role="status"`, `aria-label`, `aria-live`) - Screen reader text with `sr-only` class ### 3. Form Handling Pattern **From**: `frontend/app/signup/page.tsx` ```typescript "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { api } from "@/lib/api"; import { isValidEmail, getPasswordStrength } from "@/lib/utils"; export default function SignupPage() { const router = useRouter(); const [formData, setFormData] = useState({ name: "", email: "", password: "", }); const [errors, setErrors] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [apiError, setApiError] = useState(""); const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = "Name is required"; } if (!formData.email.trim()) { newErrors.email = "Email is required"; } else if (!isValidEmail(formData.email)) { newErrors.email = "Please enter a valid email address"; } // More validation... setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setApiError(""); if (!validateForm()) { return; } setIsLoading(true); try { const response = await api.signup(formData); if (response.success) { router.push("/dashboard"); } else { setApiError(response.message || "Signup failed"); } } catch (error: any) { setApiError(error.message || "An error occurred"); } finally { setIsLoading(false); } }; const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); // Clear error for this field when user starts typing if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: "" })); } }; return (
{/* Form fields */}
); } ``` **Pattern**: - Separate state for form data, errors, loading, API errors - Validation function that returns boolean - Clear errors on input change - Loading state during submission - Try-catch for error handling - Redirect on success ### 4. ToastNotification Pattern **From**: `frontend/components/ToastNotification.tsx` ```typescript "use client"; import { useEffect, useState } from "react"; import { ToastMessage, ToastType } from "@/types"; export function useToast() { const [toasts, setToasts] = useState([]); const showToast = (type: ToastType, message: string, duration?: number) => { const id = `toast-${Date.now()}-${Math.random()}`; const newToast: ToastMessage = { id, type, message, duration, }; setToasts((prev) => [...prev, newToast]); }; const dismissToast = (id: string) => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }; return { toasts, showToast, dismissToast, success: (message: string, duration?: number) => showToast("success", message, duration), error: (message: string, duration?: number) => showToast("error", message, duration), // ... }; } ``` **Pattern**: - Custom hook for toast management - Auto-dismiss with duration - Stack multiple toasts - Helper methods (success, error, warning, info) ## Tailwind CSS Patterns ### 1. Utility Classes Only ```typescript
``` **Pattern**: Use Tailwind utility classes, no inline styles ### 2. Conditional Classes with `cn()` Utility ```typescript import { cn } from "@/lib/utils";
``` **Pattern**: Use `cn()` from `@/lib/utils` for conditional classes ### 3. Dark Mode Support ```typescript
``` **Pattern**: Use `dark:` prefix for dark mode styles ### 4. Responsive Design ```typescript
``` **Pattern**: Use breakpoint prefixes (`sm:`, `md:`, `lg:`, `xl:`) ## Accessibility Patterns (WCAG 2.1 AA) ### 1. ARIA Labels ```typescript
``` **Pattern**: Always provide `aria-label` for icon-only buttons ### 2. Semantic HTML ```typescript ``` **Pattern**: Use semantic HTML elements (`nav`, `main`, `section`, `article`, etc.) ### 3. Keyboard Navigation ```typescript
) ); } return this.props.children; } } ``` **Pattern**: Class component, catch errors, provide fallback UI, log errors ### 7. PWA Patterns (Phase 8 - T069, T070, T071) #### Service Worker Setup ```typescript // public/sw.js or use next-pwa if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker .register("/sw.js") .then((registration) => { console.log("SW registered:", registration); }) .catch((error) => { console.error("SW registration failed:", error); }); }); } ``` **Pattern**: Register service worker on page load, handle registration errors #### IndexedDB for Offline Storage ```typescript import { openDB, DBSchema, IDBPDatabase } from "idb"; interface TaskDB extends DBSchema { tasks: { key: number; value: Task; indexes: { "by-user-id": string }; }; } export async function getDB(): Promise> { return openDB("todo-db", 1, { upgrade(db) { const taskStore = db.createObjectStore("tasks", { keyPath: "id" }); taskStore.createIndex("by-user-id", "user_id"); }, }); } export async function saveTaskOffline(task: Task) { const db = await getDB(); await db.put("tasks", task); } export async function getTasksOffline(userId: string): Promise { const db = await getDB(); return db.getAllFromIndex("tasks", "by-user-id", userId); } ``` **Pattern**: Use `idb` library for IndexedDB, create stores and indexes, handle offline data #### Offline Sync Mechanism ```typescript export async function syncOfflineChanges(userId: string) { const db = await getDB(); const offlineTasks = await db.getAllFromIndex("tasks", "by-user-id", userId); for (const task of offlineTasks) { if (task.syncStatus === "pending") { try { await api.createTask(userId, task); await db.put("tasks", { ...task, syncStatus: "synced" }); } catch (error) { console.error("Sync failed for task:", task.id, error); } } } } // Call on connection restore window.addEventListener("online", () => { syncOfflineChanges(currentUserId); }); ``` **Pattern**: Track sync status, sync on connection restore, handle sync errors ### 8. Caching Strategies (Phase 8 - T073) ```typescript // Cache API responses const cache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes export async function getCachedData(key: string, fetcher: () => Promise): Promise { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } const data = await fetcher(); cache.set(key, { data, timestamp: Date.now() }); return data; } ``` **Pattern**: Use Map for in-memory cache, check expiration, update cache on fetch ### 9. Error Logging and Tracking (Phase 8 - T075) ```typescript export function logError(error: Error, context?: Record) { console.error("Error:", error, context); // Send to error tracking service (e.g., Sentry) if (typeof window !== "undefined" && (window as any).Sentry) { (window as any).Sentry.captureException(error, { extra: context, }); } // Or send to custom endpoint fetch("/api/log-error", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: error.message, stack: error.stack, context, timestamp: new Date().toISOString(), }), }).catch((err) => console.error("Failed to log error:", err)); } ``` **Pattern**: Log to console, send to error tracking service, include context ## Common Patterns Summary 1. ✅ Use Server Components by default 2. ✅ Add `"use client"` only when needed 3. ✅ Use TypeScript interfaces for props 4. ✅ Use `cn()` utility for conditional classes 5. ✅ Always include accessibility attributes 6. ✅ Use semantic HTML elements 7. ✅ Provide loading states 8. ✅ Handle errors gracefully 9. ✅ Use Tailwind utility classes 10. ✅ Support dark mode with `dark:` prefix 11. ✅ Use `@dnd-kit/core` for drag and drop 12. ✅ Use `useReducer` for undo/redo 13. ✅ Use `setInterval` for polling with cleanup 14. ✅ Use `next/dynamic` for code splitting 15. ✅ Use ErrorBoundary for error handling 16. ✅ Use IndexedDB for offline storage 17. ✅ Sync offline changes on connection restore 18. ✅ Cache API responses with expiration 19. ✅ Log errors to tracking service