diff --git a/.dockerignore b/.dockerignore index db01e49..606ffbb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,6 +24,8 @@ coverage __tests__ **/*.test.ts **/*.spec.ts +vitest.config.ts +vitest.setup.ts # Environment files (will be provided at runtime) .env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7a37327 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 2 + +[*.{json,jsonc,yml,yaml,md,svg}] +indent_style = space +indent_size = 2 diff --git a/.env.example b/.env.example index 46a4213..5680e5a 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ # Environment Variables # Copy this file to .env -# Domain name for the application (used in metadata, SEO, sitemaps) -NEXT_PUBLIC_DOMAIN_NAME=acme.com +# Public site domain — used for canonical URLs, metadata, OpenGraph, sitemap, +# robots, and JSON-LD. NOTE: this is a NEXT_PUBLIC_* variable, so it is inlined +# into the client bundle at BUILD time. Changing it requires a rebuild. +# If unset, the app falls back to localhost:3000 (see lib/env.ts). +NEXT_PUBLIC_DOMAIN_NAME=example.com # Port to expose the application on PORT=3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..09da343 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test:run + + - name: Build + run: pnpm build + env: + NEXT_PUBLIC_DOMAIN_NAME: ci.example.com diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e08adb9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +.next +out +build +node_modules +pnpm-lock.yaml +public +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..776d39d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "tabWidth": 2, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 80 +} diff --git a/CLAUDE.md b/CLAUDE.md index e1fdbbf..1be6c54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co pnpm dev # Start dev server pnpm build # Production build (also runs TypeScript checking) pnpm lint # ESLint +pnpm typecheck # tsc --noEmit +pnpm test:run # Vitest (run once); `pnpm test` for watch +pnpm format # Prettier pnpm start # Start production server ``` @@ -22,50 +25,43 @@ make down # Stop containers make logs # View logs ``` -No test framework is configured. +Testing uses **Vitest** (`*.test.ts`, run with `pnpm test:run`). CI runs lint + typecheck + test + build (`.github/workflows/ci.yml`). ## Architecture -**Next.js 16 App Router** with React 19, Tailwind CSS v4, and shadcn/ui (base-nova style, Tabler icons). +**Next.js 16 App Router** with React 19, Tailwind CSS v4, and shadcn/ui (base-nova style on **@base-ui/react** — no Radix), Tabler icons, Jotai. -### Key patterns - -- **Server Components by default.** Only add `"use client"` for interactive components (motion animations, event handlers, forms with state). -- **State Management**: Uses **Jotai** for atomic state. Atoms are defined in `lib/store/*.ts`. Use `useAtom`, `useAtomValue`, or `useSetAtom` in client components. -- **Async Dynamic APIs (Next.js 15/16)**: In server components (like `page.tsx` or `generateMetadata`), dynamic APIs like `params` and `searchParams` are Promises and must be awaited before accessing properties. -- **Layout wrapper**: `components/layout/Layout.tsx` wraps pages with `Header` + `Footer` + `CartSidebar`. Used in page components, not in `app/layout.tsx`. -- **Section components**: Page content is built from `components/sections/*.tsx` modules composed in page files. -- **SEO infrastructure**: `app/layout.tsx` has full metadata (OG, Twitter, robots) + JSON-LD via `components/JsonLd.tsx` using `schema-dts`. Sitemap auto-generates from filesystem in `app/sitemap.ts`. -- **Animations**: Use `motion/react`. Import as `import { motion } from "motion/react"`. -- **Styling**: Tailwind v4 with `@theme` block in `globals.css`. Color tokens use OKLCH. Use `cn()` from `lib/utils.ts` for class merging. -- **Toast notifications**: Sonner is integrated. `` in root layout. Use `import { toast } from "sonner"`. - -### Environment +### Data, types & config layer (the foundation) -Requires `NEXT_PUBLIC_DOMAIN_NAME` (see `.env.example`). Used by metadata, robots.txt, and sitemap generation. +- **Single source of truth**: catalog data is in `data/products.ts` and `data/categories.ts`, validated by zod at module load. Don't inline product/category data in components. +- **Accessors**: read data through the async functions in `lib/data/products.ts` (`getAllProducts`, `getProductById`, `getProductsByCategory`, `getProductsOnSale`, `getFeaturedProducts`, `getSearchIndex`). Swap this module for a real backend. +- **Types**: `Product`/`Category` are derived from zod schemas in `lib/schemas/`. Import domain types from `@/types` (`Product`, `Category`, `CartItem`) — never re-declare them inline. +- **Money is integer cents** (`priceCents`). Format only at the view with `formatPrice` from `lib/format.ts`. +- **Brand/SEO/nav** live in `lib/config/site.ts` (`siteConfig`). Env is validated in `lib/env.ts` (`siteUrl`). -### Docker Configuration +### Key patterns -- **Dockerfile**: Multi-stage build optimized for Next.js standalone output -- **docker-compose.yml**: Production orchestration with health checks and resource limits -- **.dockerignore**: Optimized build context exclusions -- **output: "standalone"** in `next.config.ts` enables minimal production image (~150-200MB) -- Security: Non-root user (nextjs:1001), Alpine base, read-only where possible -- See `DOCKER.md` for comprehensive deployment guide +- **Server Components by default.** Add `"use client"` only on the smallest interactive leaf. Pattern: `components/products/ProductCard.tsx` is the client island; `ProductGrid` and the sections that compose it stay on the server. `motion/react` is used **only** in `app/not-found.tsx` — other entrance animations are CSS (`tw-animate-css`). +- **State (Jotai)**: atoms in `lib/store/cartStore.ts`. `addToCartAtom` is a pure data action (callers open the cart). Guard persisted reads with `useIsHydrated` (`lib/hooks/`). +- **Async Dynamic APIs**: `params`/`searchParams` are Promises — await before access. +- **Layout wrapper**: `components/layout/Layout.tsx` wraps pages with `Header` + `
` + `Footer` + `CartSidebar`. The skip link and `ThemeProvider` live in `app/layout.tsx`. +- **SEO**: root metadata + WebSite/Organization JSON-LD in `app/layout.tsx`; per-page metadata via `pageMetadata` (`lib/seo.ts`); Product + BreadcrumbList JSON-LD on product pages. `app/products/[id]` is SSG via `generateStaticParams`; `app/sitemap.ts` includes product routes. +- **Styling**: Tailwind v4 `@theme` tokens (OKLCH) in `globals.css`, incl. `success`/`warning`/`rating`. Use `cn()`; avoid raw palette colors and `var()` in `className`. Dark mode via `next-themes` + the `.dark` token block; toggle in the Header. +- **Forms**: react-hook-form + zod (see `components/forms/ContactForm.tsx`). +- **Toasts**: Sonner; `` in root layout. -### Font setup +### Environment -Uses **Outfit** as the primary sans-serif font loaded via `next/font/google` in `app/layout.tsx`. Font display set to `swap`. +`NEXT_PUBLIC_DOMAIN_NAME` (see `.env.example`) — used for metadata, canonical URLs, sitemap, robots, JSON-LD. It is **build-time only** (inlined into the client bundle). Validated in `lib/env.ts`, which falls back to `localhost:3000`. -### Images +### Docker -Always use `next/image` with `fill` + `sizes` or explicit dimensions. Social sharing images in `/public`. +Two-stage Dockerfile (`builder` → `runner`) with `output: "standalone"`, non-root user, and a health check against `/api/health` (matched by `docker-compose.yml`). pnpm version comes from `package.json` `packageManager`. See `DOCKER.md`. ### Pages -- `/` - Home page with hero, categories, and featured products -- `/products` - Full product collection page -- `/products/[id]` - Dynamic product detail page (Async Params pattern) -- `/men`, `/women`, `/accessories`, `/sale` - Category collection pages -- `/contact`, `/shipping`, `/returns`, `/faq`, `/privacy`, `/terms` - Informational and customer service pages -- `/not-found` - Custom 404 page with animations +- `/` — hero, categories, featured products +- `/products` — full collection; `/products/[id]` — SSG product detail (Async Params) +- `/men`, `/women`, `/accessories` — category-filtered; `/sale` — on-sale products +- `/contact` (validated form), `/shipping`, `/returns`, `/faq`, `/privacy`, `/terms` +- `not-found.tsx` (animated 404, reduced-motion aware), `error.tsx` (route error boundary), `api/health` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..71ee654 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing + +Thanks for your interest in improving this template! + +## Prerequisites + +- **Node.js 24** (see `.nvmrc` — run `nvm use`) +- **pnpm 9+** (the repo pins a version via `packageManager`; `corepack enable` will respect it) + +## Setup + +```bash +pnpm install +cp .env.example .env # then set NEXT_PUBLIC_DOMAIN_NAME +pnpm dev +``` + +## Checks (run before opening a PR) + +```bash +pnpm lint # ESLint +pnpm typecheck # tsc --noEmit +pnpm test:run # Vitest +pnpm build # production build (also type-checks) +``` + +CI (`.github/workflows/ci.yml`) runs the same steps on every push and PR. + +## Conventions + +- **Server Components by default.** Add `"use client"` only to the smallest leaf that needs interactivity (see `components/products/ProductCard.tsx` as the pattern). +- **Data lives in one place.** Product/category data is in `data/`; read it through the accessors in `lib/data/products.ts`. Don't inline catalog data in components. +- **Types come from the schema.** `Product`/`Category` are derived from the zod schemas in `lib/schemas/`; import domain types from `@/types`. +- **Money is integer cents.** Format for display with `formatPrice` from `lib/format.ts`. +- **Brand/SEO strings come from `lib/config/site.ts`.** Per-page metadata uses the `pageMetadata` helper in `lib/seo.ts`. +- **Components use named exports** and live under intent-based folders (`layout/`, `sections/`, `products/`, `cart/`, `forms/`, `providers/`, `ui/`). +- **Styling:** Tailwind v4 semantic tokens (`bg-primary`, `text-muted-foreground`, …) defined in `app/globals.css` — avoid raw palette colors and `var()` inside `className`. Use `cn()` for class merging. + +## Adding shadcn/ui primitives + +Only the primitives the template uses are committed. Add more on demand: + +```bash +pnpm dlx shadcn add +``` diff --git a/DOCKER.md b/DOCKER.md index 94b1372..4e7e097 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -27,7 +27,7 @@ NEXT_PUBLIC_DOMAIN_NAME=example.com PORT=3000 ``` -**Important:** `NEXT_PUBLIC_DOMAIN_NAME` is required at build time for proper SEO and metadata generation. +**Important:** `NEXT_PUBLIC_DOMAIN_NAME` is a **build-time** value — Next.js inlines `NEXT_PUBLIC_*` variables into the client bundle during `pnpm build`. It is passed as a Docker **build arg** (see below). Changing it requires rebuilding the image, not just restarting the container; setting it only as a runtime `environment` value has no effect on already-built client code. ### Build Arguments @@ -52,9 +52,10 @@ docker build --build-arg NEXT_PUBLIC_DOMAIN_NAME=yourdomain.com -t website-templ **Stages:** -1. **deps**: Install production dependencies -2. **builder**: Build the Next.js application -3. **runner**: Minimal runtime with only necessary files +1. **builder**: Install dependencies and build the Next.js application +2. **runner**: Minimal runtime serving the standalone output + +(The standalone output traces its own runtime `node_modules`, so no separate production-deps stage is needed.) The base image is `node:24-alpine`; the pnpm version is taken from `package.json` `"packageManager"` via corepack. ## Manual Docker Commands @@ -69,21 +70,22 @@ docker build \ ### Run Production Container +The domain is baked in at build time (above), so the run command does not need it: + ```bash docker run -d \ --name website-template \ -p 3000:3000 \ - -e NEXT_PUBLIC_DOMAIN_NAME=example.com \ website-template:latest ``` ## Health Checks -Production containers include health checks via `docker-compose.yml`: +Both the Dockerfile `HEALTHCHECK` and `docker-compose.yml` probe the app's health route: - Checks every 30s - 40s startup grace period -- Endpoint: `http://localhost:3000` +- Endpoint: `http://localhost:3000/api/health` (returns `{ "status": "ok" }`) ## Resource Limits @@ -186,7 +188,7 @@ make prune ## CI/CD Integration -### GitHub Actions Example +A ready-to-use workflow at `.github/workflows/ci.yml` runs lint, typecheck, test, and build on every push/PR. To also build the Docker image, add a step: ```yaml - name: Build Docker Image diff --git a/Dockerfile b/Dockerfile index 4795472..07440ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,105 +1,65 @@ -# Multi-stage Dockerfile for Next.js 16.1.6 with pnpm -# Optimized for production with minimal image size and security hardening +# Multi-stage Dockerfile for Next.js 16.2.9 with pnpm +# Optimized for production with minimal image size and security hardening. +# pnpm version is taken from package.json "packageManager" via corepack. # ============================================ -# Stage 1: Dependencies -# Install only production dependencies for caching optimization +# Stage 1: Builder — install all deps and build # ============================================ -FROM node:22-alpine AS deps +FROM node:24-alpine AS builder -# Install necessary system dependencies for native modules +# System dependencies for native modules RUN apk add --no-cache libc6-compat -# Enable pnpm +# Enable pnpm (version resolved from package.json "packageManager") ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable && corepack prepare pnpm@latest --activate +RUN corepack enable WORKDIR /app -# Copy dependency manifests for optimal layer caching +# Install dependencies first for optimal layer caching COPY package.json pnpm-lock.yaml* ./ - -# Install dependencies with frozen lockfile for reproducibility -RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ - pnpm install --frozen-lockfile --prod - -# ============================================ -# Stage 2: Builder -# Build the Next.js application -# ============================================ -FROM node:22-alpine AS builder - -# Install system dependencies -RUN apk add --no-cache libc6-compat - -# Enable pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable && corepack prepare pnpm@latest --activate - -WORKDIR /app - -# Copy dependency manifests -COPY package.json pnpm-lock.yaml* ./ - -# Install all dependencies (including dev dependencies for build) RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile -# Copy application source code +# Copy source and build COPY . . -# Build arguments for environment variables (build-time) -# NEXT_PUBLIC_* variables must be available at build time +# NEXT_PUBLIC_* values are inlined at BUILD time, so the domain must be set here. +# Changing it later requires a rebuild, not just a container restart. ARG NEXT_PUBLIC_DOMAIN_NAME ENV NEXT_PUBLIC_DOMAIN_NAME=$NEXT_PUBLIC_DOMAIN_NAME - -# Disable Next.js telemetry ENV NEXT_TELEMETRY_DISABLED=1 -# Build the Next.js application -# Next.js will automatically use standalone output mode if configured RUN pnpm build # ============================================ -# Stage 3: Runner (Production) -# Minimal runtime image with only necessary files +# Stage 2: Runner — minimal production image # ============================================ -FROM node:22-alpine AS runner +FROM node:24-alpine AS runner WORKDIR /app -# Create non-root user for security +# Non-root user RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs -# Set production environment ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -# Copy only necessary files from builder -# Copy public assets +# Standalone output bundles its own traced node_modules. COPY --from=builder /app/public ./public - -# Copy Next.js standalone output (if using output: 'standalone' in next.config) -# Otherwise copy .next directory COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -# Switch to non-root user USER nextjs -# Expose port (default Next.js port) EXPOSE 3000 - -# Set default port ENV PORT=3000 ENV HOSTNAME="0.0.0.0" -# Health check to ensure container is healthy +# Probes the real /api/health route (matches docker-compose.yml). HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" || exit 1 + CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))" -# Start the Next.js application CMD ["node", "server.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2762fa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fashion Oasis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LLMS.md b/LLMS.md index 12f52e3..6533580 100644 --- a/LLMS.md +++ b/LLMS.md @@ -1,125 +1,75 @@ # LLM Resource: Website Template Guide -This file serves as a comprehensive entry point for LLMs (like Claude, ChatGPT, or Gemini) to understand, utilize, and extend this website template. It outlines the core architecture, styling conventions, and optimization patterns used in the project. +Entry point for LLMs working in this repo. It describes the architecture, conventions, and the data/types/config layer that everything depends on. ## Project Overview -- **Framework**: Next.js 16.1.6 (App Router) -- **Runtime**: React 19.2.4 -- **State Management**: Jotai (Atomic state) -- **Styling**: Tailwind CSS v4 with OKLCH color space -- **Typography**: Outfit font (Google Fonts, optimized with `display: swap`) -- **Animations**: Motion v12 (`motion/react`) -- **UI Components**: shadcn/ui Base Nova (Base UI + Radix UI Primitives) -- **Icons**: Tabler Icons v3 (`@tabler/icons-react`) -- **Notifications**: Sonner toast library -- **Type Safety**: TypeScript 5.9.3 (Strict Mode) -- **Package Manager**: pnpm -- **Containerization**: Docker with multi-stage builds, standalone output +- **Framework**: Next.js 16 (App Router) +- **Runtime**: React 19 +- **Language**: TypeScript 6 (strict, plus `noUncheckedIndexedAccess`, `noUnusedLocals/Parameters`, `noImplicitOverride`) +- **State**: Jotai (atomic) +- **Styling**: Tailwind CSS v4 with OKLCH tokens (`app/globals.css`) +- **UI**: shadcn/ui (base-nova style) on **@base-ui/react** — there is **no Radix UI** dependency +- **Icons**: Tabler (`@tabler/icons-react`); **Animations**: Motion (404 only); **Toasts**: Sonner +- **Validation**: zod 4 + react-hook-form +- **Testing**: Vitest; **Package manager**: pnpm (pinned via `packageManager`); **Container**: Docker (standalone) + +## The layer everything depends on (read this first) + +- `data/products.ts`, `data/categories.ts` — the single source of truth (mock catalog), validated by zod at load. +- `lib/schemas/product.ts` — zod schemas; `Product`/`Category` types are `z.infer`red from here. +- `types/index.ts` — import domain types from `@/types` (`Product`, `Category`, `CartItem`). Never re-declare these inline. +- `lib/data/products.ts` — async accessors (`getAllProducts`, `getProductById`, `getProductsByCategory`, `getProductsOnSale`, `getFeaturedProducts`) + `getSearchIndex`. Pages call these; swap this module for a real backend. +- `lib/config/site.ts` — brand/SEO/nav (`siteConfig`). All brand strings come from here. +- `lib/env.ts` — validates `NEXT_PUBLIC_DOMAIN_NAME`, exports `siteUrl`. +- `lib/format.ts` — `formatPrice(cents)`. **Money is integer cents**; format only at the view. +- `lib/seo.ts` — `pageMetadata({ title, description, path })` for per-page canonical + OG/Twitter. ## Directory Structure -- `/app`: App Router core - - `layout.tsx`: Root layout with SEO metadata, fonts, Toaster - - `page.tsx`: Home page (Hero + Categories + Featured Products) - - `products/[id]/page.tsx`: Dynamic product details (Async Params pattern) - - `not-found.tsx`: Custom animated 404 page - - `sitemap.ts`: Dynamic sitemap with route group support -- `/components`: - - `/ui`: 56 shadcn/ui components - - `/cart`: CartSidebar component and logic - - `/layout`: Header (sticky nav), SearchMenu, Footer - - `/sections`: HeroSection, CategoriesSection, FeaturedProductsSection -- `/lib`: - - `/store`: Jotai atoms (`cartStore.ts`) - - `/hooks`: Custom utility hooks (`useIsHydrated.ts`) - - `utils.ts`: `cn()` for Tailwind class merging -- `/public`: Static assets (og-image.png, logo.png, PWA manifest) +- `/app`: routes (RSC). `layout.tsx` (metadata + JSON-LD + ThemeProvider + skip link), `products/[id]/page.tsx` (SSG via `generateStaticParams`, Product/Breadcrumb JSON-LD), category routes, `api/health/route.ts`, `sitemap.ts`, `robots.ts`, `manifest.ts`, `not-found.tsx`, `error.tsx`. +- `/components`: `ui/` (primitives), `layout/`, `sections/`, `products/` (ProductCard/ProductGrid/ProductActions), `cart/`, `forms/`, `providers/`. +- `/lib`, `/data`, `/types`: as above. ## Core Implementation Patterns -### 1. State Management (Jotai) +### 1. Server Components by default +Add `"use client"` only on the smallest interactive leaf. Example: `components/products/ProductCard.tsx` is the client island; `ProductGrid` and the sections that use it stay on the server. The add-to-cart control is a real `