diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ee35852 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,939 @@ +# CLAUDE.md + +## Project Overview + +**GridTip** is an F1 season tipping application where users predict race outcomes and compete with friends in groups. + +### Core Concepts + +- **Groups**: Users create or join groups to compete with friends +- **Predictions/Tips**: Users predict race outcomes (pole, P1, P10, last place, sprint results, etc.) +- **Cutoff System**: Tips lock before qualifying/sprint qualifying +- **Leaderboard**: Track standings and compare predictions with group members +- **Admin Controls**: Group admins can manage tips and manually override scoring +- **Championship Predictions**: Season-long predictions for driver and constructor championships + +### Data Source + +All F1 data (races, drivers, constructors, results) comes from the **Jolpica F1 API**: + +- API Base: `https://api.jolpi.ca/ergast/f1/` +- Documentation: [jolpica-f1 API docs](https://github.com/jolpica/jolpica-f1/blob/main/docs/README.md) +- Updated via protected API endpoints (see API Routes section) + +--- + +## Tech Stack + +### Core Framework + +- **Next.js** 15.5.2 (App Router, React Server Components) +- **React** 19.1.0 +- **TypeScript** 5.x (strict mode) +- **Bun** (runtime and package manager) + +### Database & ORM + +- **Turso** (SQLite edge database) +- **Drizzle ORM** +- **Convention**: `snake_case` for database columns + +### Authentication + +- **better-auth** +- **Google OAuth** provider + +### UI & Styling + +- **Tailwind CSS** 4.x +- **shadcn/ui** (New York style) +- **Lucide React** for icons + +### Forms & Validation + +- **react-hook-form** +- **Zod** 4 for schema validation + +### Other Key Libraries + +- **@tanstack/react-table** 8.21.3 (admin tables) +- **date-fns** 4.1.0 (date manipulation) +- **ofetch** 1.4.1 (HTTP client) +- **sonner** 2.0.7 (toast notifications) +- **marked** 16.4.0 (markdown parsing) + +### Development Tools + +- **Vitest** 3.2.4 (testing with jsdom) +- **ESLint** 9.x + Prettier +- **drizzle-kit** (migrations) + +--- + +## Database Schema + +### Application Tables + +#### `groups` + +```typescript +{ + id: string (cuid2, PK) + name: string + adminUser: string (FK → user.id) + createdAt: timestamp + cutoffInMinutes: number (default: 180) + iconName: enum (default: 'lucide:users') +} +``` + +#### `group_members` + +```typescript +{ + id: string (cuid2, PK) + groupId: string (FK → groups.id, CASCADE) + userId: string (FK → user.id, CASCADE) + joinedAt: timestamp +} +``` + +#### `races` + +```typescript +{ + id: string (circuit ID, PK) + country: string + locality: string + round: number + circuitName: string + raceName: string + grandPrixDate: timestamp + qualifyingDate: timestamp + sprintDate: timestamp | null + sprintQualifyingDate: timestamp | null + lastUpdated: timestamp + created: timestamp +} +``` + +#### `drivers` + +```typescript +{ + id: string (PK) + permanentNumber: string + fullName: string + givenName: string + familyName: string + nationality: string + constructorId: string (FK → constructors.id, CASCADE) + lastUpdated: timestamp + created: timestamp +} +``` + +#### `constructors` + +```typescript +{ + id: string(PK) + name: string + nationality: string + created: timestamp + lastUpdated: timestamp +} +``` + +#### `predictions` + +```typescript +{ + id: string (cuid2, PK) + userId: string (FK → user.id, CASCADE) + groupId: string (FK → groups.id, CASCADE) + isForChampionship: boolean (default: false) + raceId: string | null (FK → races.id, CASCADE) + createdAt: timestamp + + // Indexes on: userId, groupId, isForChampionship, raceId +} +``` + +#### `prediction_entries` + +```typescript +{ + id: string (cuid2, PK) + predictionId: string (FK → predictions.id, CASCADE) + position: enum (PREDICTION_FIELDS) + driverId: string | null (FK → drivers.id, CASCADE) + constructorId: string | null (FK → constructors.id, CASCADE) + overwriteTo: 'countAsCorrect' | 'countAsIncorrect' | null + createdAt: timestamp + + // Unique constraint: (predictionId, position) +} +``` + +#### `results` + +```typescript +{ + id: string (cuid2, PK) + raceId: string (FK → races.id, CASCADE) + driverId: string | null (FK → drivers.id, CASCADE) + constructorId: string (FK → constructors.id, CASCADE) + sprint: number | null + grid: number | null + position: number | null + points: number + status: string + addedAt: timestamp + updatedAt: timestamp +} +``` + +### Authentication Tables + +#### `user` + +```typescript +{ + id: string(PK) + name: string + email: string(unique) + emailVerified: boolean + image: string | null + createdAt: timestamp + updatedAt: timestamp +} +``` + +#### `session` + +```typescript +{ + id: string (PK) + token: string (unique) + expiresAt: timestamp + ipAddress: string | null + userAgent: string | null + userId: string (FK → user.id, CASCADE) + createdAt: timestamp + updatedAt: timestamp +} +``` + +#### `account` + +```typescript +{ + id: string (PK) + accountId: string + providerId: string + userId: string (FK → user.id, CASCADE) + accessToken: string | null + refreshToken: string | null + idToken: string | null + accessTokenExpiresAt: timestamp | null + refreshTokenExpiresAt: timestamp | null + scope: string | null + password: string | null + createdAt: timestamp + updatedAt: timestamp +} +``` + +#### `verification` + +```typescript +{ + id: string(PK) + identifier: string + value: string + expiresAt: timestamp + createdAt: timestamp + updatedAt: timestamp +} +``` + +### Prediction Fields Constants + +```typescript +// Race prediction fields +DRIVER_RACE_PREDICTION_FIELDS = ['sprintP1', 'pole', 'p1', 'p10', 'last'] +CONSTRUCTOR_RACE_PREDICTION_FIELDS = ['constructorWithMostPoints'] + +// Championship prediction fields +CHAMPIONSHIP_PREDICTION_FIELDS = [ + 'championshipConstructor', + 'championshipDriver', +] + +// Cutoff reference mapping +CUTOFF_REFERENCE_KEY = { + pole: 'qualifyingDate', + p1: 'qualifyingDate', + p10: 'qualifyingDate', + last: 'qualifyingDate', + constructorWithMostPoints: 'qualifyingDate', + sprintP1: 'sprintQualifyingDate', +} +``` + +**Location**: `/home/user/gridtip/db/schema/schema.ts` + +### Key Functions (DAL - Data Access Layer) + +Location: `/home/user/gridtip/lib/dal.ts` + +```typescript +// Verify session (cached, redirects if not authenticated) +await verifySession() + +// Optional session retrieval +const session = await getMaybeSession() + +// Get member status (Admin | Member) +const status = await getMemberStatus(groupId) + +// Verify admin status (cached) +await verifyIsAdmin(groupId) +``` + +### Protected Routes + +**Middleware** (`/home/user/gridtip/app/middleware.ts`): + +- Protected path: `/tipping/*` +- Redirects to: `/auth?origin=not-logged-in` + +### API Route Protection + +Update endpoints use Basic Auth (`validateToken()` in `/home/user/gridtip/app/api/utils.ts`): + +- Requires `UPDATES_USER` and `UPDATES_PASSWORD_HASH` +- Applied to: `/api/races/update`, `/api/drivers/update`, `/api/constructors/update`, `/api/results/update` + +--- + +## Code Conventions & Style + +### TypeScript Conventions + +1. **Strict mode enabled** - All TypeScript strict checks active +2. **Path aliases**: + - `@/*` → project root + - `@@/*` → `/app` directory +3. **Type imports**: Use `import type` for type-only imports +4. **Database types**: Generated in `/home/user/gridtip/db/types` + +### Naming Conventions + +1. **Files & Directories**: + - Components: `kebab-case.tsx` (e.g., `group-switcher.tsx`) + - Routes: `kebab-case` (e.g., `add-tips/`) + - Utilities: `kebab-case.ts` (e.g., `prediction-fields.ts`) + +2. **Database**: + - Tables: `snake_case` (e.g., `group_members`) + - Columns: `snake_case` (enforced by Drizzle config) + +3. **React Components**: + - PascalCase (e.g., `GroupSwitcher`) + - Use named exports for consistency + +4. **Functions**: + - camelCase (e.g., `getMemberStatus`) + - Server actions: descriptive names (e.g., `createGroup`) + +5. **Constants**: + - SCREAMING_SNAKE_CASE (e.g., `DEFAULT_CUTOFF_MINS`) + - Enums: PascalCase (e.g., `CacheTag.Results`) + +### Component Organization + +**Colocation pattern** - Route-specific code lives with the route: + +``` +app/tipping/add-tips/[race-id]/ +├── page.tsx # Route component +├── _components/ # Route-specific components +│ └── tip-form.tsx +├── actions/ # Route-specific actions +│ └── submit-tips.ts +└── _utils/ # Route-specific utilities + └── validation.ts +``` + +## Key Features & Routes + +### Public Routes (`app/(public)/`) + +- **`/`** - Landing page +- **`/contact`** - Contact information + +### Standalone Routes (`app/(standalone)/`) + +- **`/auth`** - Authentication page (Google OAuth) +- **`/join/[groupId]`** - Join group via shareable link + +### Protected Routes (`app/tipping/`) + +#### Dashboard (`/tipping`) + +**File**: `/home/user/gridtip/app/tipping/page.tsx` + +Dynamic cards based on state: + +- Join/create group prompt (if no groups) +- Next race tipping card (with cutoff countdown) +- Tipping status (who has/hasn't tipped) +- Everyone's tips (accordion, locked before cutoff) +- Previous race results + +#### Add Tips (`/tipping/add-tips/[race-id]`) + +**File**: `/home/user/gridtip/app/tipping/add-tips/[race-id]/page.tsx` + +- Predict: pole, P1, P10, last place, constructor with most points +- Sprint races: also predict sprint P1 +- Cutoff enforcement (default: 180 min before qualifying) +- Edit existing tips before cutoff +- Form validation via Zod + +#### Championships (`/tipping/championships`) + +**File**: `/home/user/gridtip/app/tipping/championships/page.tsx` + +- Predict championship winner (driver) +- Predict championship winner (constructor) +- Season-long predictions + +#### Leaderboard (`/tipping/leaderboard`) + +**File**: `/home/user/gridtip/app/tipping/leaderboard/page.tsx` + +- View race results +- Group standings +- Past race predictions vs. actual results +- Results comparison table + +#### Groups (`/tipping/groups`) + +**File**: `/home/user/gridtip/app/tipping/groups/page.tsx` + +- Create new group +- Edit group settings (name, icon, cutoff time) +- View group members +- Leave group + +#### Group Admin (`/tipping/group-admin`) + +**File**: `/home/user/gridtip/app/tipping/group-admin/page.tsx` + +**Requires**: Admin status + +Features: + +- View all user tips in data table (@tanstack/react-table) +- Edit/create tips on behalf of users +- Admin overwrite system: + - Mark predictions as correct (`countAsCorrect`) + - Mark predictions as incorrect (`countAsIncorrect`) +- Update results button (clears cache) + +#### Settings (`/tipping/settings`) + +**File**: `/home/user/gridtip/app/tipping/settings/page.tsx` + +- Account deletion +- Profile information + +### API Routes (`app/api/`) + +#### Authentication + +- **`POST/GET /api/auth/[...all]`** - better-auth catch-all handler + +#### Data Updates (Protected with Basic Auth) + +All require `UPDATES_USER` and `UPDATES_PASSWORD_HASH`: + +- **`GET /api/races/update`** - Fetch and update race data from Jolpica API +- **`GET /api/drivers/update`** - Update driver data +- **`GET /api/constructors/update`** - Update constructor data +- **`GET /api/results/update`** - Update race results + +**API Utils** (`app/api/utils.ts`): + +```typescript +fetchJolpica() // ofetch instance for Jolpica API +validateToken() // Basic auth validation +createResponse() // Standardized JSON responses +areFieldsTheSame() // Compare objects for updates +``` + +--- + +## Common Patterns + +### React Server Components (Default) + +Most page and layout components are RSCs: + +```typescript +// ✅ Server Component (default) +export default async function Page() { + const data = await db.query.races.findMany() + return
{/* ... */}
+} +``` + +### Client Components + +Use `"use client"` directive when needed: + +```typescript +// ✅ Client Component +'use client' + +import { useState } from 'react' + +export function Counter() { + const [count, setCount] = useState(0) + // ... +} +``` + +**When to use Client Components**: + +- Forms with `react-hook-form` +- Interactive UI (combobox, dialogs) +- Components using hooks (`useState`, `useEffect`) +- Event handlers + +### Server Actions + +**Pattern** (see `/home/user/gridtip/actions/`): + +```typescript +'use server' + +import { z } from 'zod' +import { verifySession } from '@/lib/dal' +import { db } from '@/db' +import { ServerResponse } from '@/types' + +const schema = z.object({ + name: z.string().min(1), +}) + +export async function createGroup( + input: z.infer, +): Promise { + // 1. Verify session + const { user } = await verifySession() + + // 2. Validate input + const validatedInput = schema.parse(input) + + // 3. Database operations + const [group] = await db + .insert(groups) + .values({ + name: validatedInput.name, + adminUser: user.id, + }) + .returning() + + // 4. Return standardized response + return { ok: true, message: 'Group created' } +} +``` + +**Response type**: + +```typescript +type ServerResponse = { + ok: boolean + message: string +} +``` + +### Data Fetching & Caching + +**Pattern 1: `cache()` for request memoization** + +```typescript +import { cache } from 'react' + +export const getDrivers = cache(async () => { + return db.query.drivers.findMany() +}) +``` + +**Pattern 2: `unstable_cache()` for persistence** +Cache data aggressively. Most data only updates on specific triggers. + +```typescript +import { unstable_cache } from 'next/cache' +import { CacheTag } from '@/constants/cache' + +export const getRaces = unstable_cache( + async () => db.query.races.findMany(), + [], + { tags: [CacheTag.Races] }, +) +``` + +**Cache Tags** (`/home/user/gridtip/constants/cache.ts`): + +```typescript +enum CacheTag { + Results = 'results', + Constructors = 'constructors', + Drivers = 'drivers', + Races = 'races', + Predictions = 'predictions', +} +``` + +**Revalidation**: + +```typescript +import { revalidateTag } from 'next/cache' +import { CacheTag } from '@/constants/cache' + +// Revalidate specific cache tag +revalidateTag(CacheTag.Races) +``` + +### Component Composition + +**Pattern**: Small, focused components + +```typescript +// ✅ Good - Composable components +function CardJoinGroup() { + return {/* ... */} +} + +function CardTipNext({ race, groupId }: Props) { + return {/* ... */} +} + +export default function Dashboard() { + return ( +
+ {!hasGroups && } + {currentRace && } +
+ ) +} +``` + +### Type Safety + +**Database types** (`/home/user/gridtip/db/types`): + +```typescript +import { Database } from '@/db/types' + +type User = Database.User +type Race = Database.Race +``` + +**Const assertions for type narrowing**: + +```typescript +export const PREDICTION_FIELDS = ['pole', 'p1', 'p10', 'last'] as const + +type PredictionField = (typeof PREDICTION_FIELDS)[number] +// Type: 'pole' | 'p1' | 'p10' | 'last' +``` + +--- + +## Common Tasks + +### Adding a New Route + +1. **Create route directory**: + + ``` + app/tipping/my-feature/ + ├── page.tsx + ├── _components/ + ``` + +2. **Add to navigation** (if needed): + - Edit: `components/nav-main.tsx` + - Add route to `items` array + +3. **Add breadcrumb** (if needed): + - Edit: `components/breadcrumbs.tsx` + +### Adding a New Database Table + +1. **Define schema** in `/home/user/gridtip/db/schema/schema.ts`: + + ```typescript + export const myTable = sqliteTable('my_table', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + name: text('name').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }) + .notNull() + .$defaultFn(() => new Date()), + }) + ``` + +2. **Generate migration**: + + ```bash + bun run db:generate + ``` + +3. **Review migration** in `db/migrations/` + +4. **Apply migration**: + + ```bash + bun run db:migrate + ``` + +5. **Add to Drizzle queries** (optional): + ```typescript + // In db/index.ts + export const db = drizzle(client, { + schema: { ...authSchema, myTable }, + }) + ``` + +### Adding a New Server Action + +1. **Create file** in `/actions/` or route-specific `actions/`: + + ```typescript + 'use server' + + import { z } from 'zod' + import { verifySession } from '@/lib/dal' + import { ServerResponse } from '@/types' + + const schema = z.object({ + // Define schema + }) + + export async function myAction( + input: z.infer, + ): Promise { + const { user } = await verifySession() + const validated = schema.parse(input) + + // Perform action + + return { ok: true, message: 'Success' } + } + ``` + +2. **Use in component**: + + ```typescript + import { myAction } from '@/actions/my-action' + + const result = await myAction({ + /* data */ + }) + if (result.ok) { + toast.success(result.message) + } + ``` + +### Adding a New shadcn/ui Component + +```bash +# Install individual component +bunx shadcn@latest add [component-name] +``` + +**Example**: + +```bash +bunx shadcn@latest add tabs +``` + +**Configuration**: `components.json` + +### Invalidating Cache + +```typescript +import { revalidateTag } from 'next/cache' +import { CacheTag } from '@/constants/cache' + +// Invalidate specific tag +revalidateTag(CacheTag.Predictions) + +// Clear multiple tags +revalidateTag(CacheTag.Results) +revalidateTag(CacheTag.Predictions) +``` + +--- + +## Important File References + +### Core Configuration + +| File | Purpose | Location | +| ------------------- | ---------------------- | -------------------------------------- | +| `package.json` | Dependencies & scripts | `/home/user/gridtip/package.json` | +| `tsconfig.json` | TypeScript config | `/home/user/gridtip/tsconfig.json` | +| `next.config.ts` | Next.js config | `/home/user/gridtip/next.config.ts` | +| `drizzle.config.ts` | Database config | `/home/user/gridtip/drizzle.config.ts` | +| `eslint.config.mjs` | Linting rules | `/home/user/gridtip/eslint.config.mjs` | +| `.prettierrc` | Code formatting | `/home/user/gridtip/.prettierrc` | +| `vitest.config.mts` | Test config | `/home/user/gridtip/vitest.config.mts` | +| `components.json` | shadcn config | `/home/user/gridtip/components.json` | + +### Database + +| File | Purpose | Location | +| ---------------- | --------------- | --------------------------------------------- | +| `schema.ts` | Main schema | `/home/user/gridtip/db/schema/schema.ts` | +| `auth-schema.ts` | Auth schema | `/home/user/gridtip/db/schema/auth-schema.ts` | +| `index.ts` | DB client | `/home/user/gridtip/db/index.ts` | +| `migrations/` | Migration files | `/home/user/gridtip/db/migrations/` | + +### Authentication + +| File | Purpose | Location | +| ---------------- | ------------------ | --------------------------------------- | +| `auth.ts` | Server auth config | `/home/user/gridtip/lib/auth.ts` | +| `auth-client.ts` | Client auth | `/home/user/gridtip/lib/auth-client.ts` | +| `dal.ts` | Data Access Layer | `/home/user/gridtip/lib/dal.ts` | +| `middleware.ts` | Route protection | `/home/user/gridtip/app/middleware.ts` | + +### Constants + +| File | Purpose | Location | +| --------------- | -------------- | -------------------------------------------- | +| `index.ts` | Core constants | `/home/user/gridtip/constants/index.ts` | +| `cache.ts` | Cache tags | `/home/user/gridtip/constants/cache.ts` | +| `icon-names.ts` | Group icons | `/home/user/gridtip/constants/icon-names.ts` | + +### Utilities + +| File | Purpose | Location | +| ---------------------- | ------------------ | --------------------------------------------------- | +| `utils.ts` | General utils (cn) | `/home/user/gridtip/lib/utils.ts` | +| `groups.ts` | Group utilities | `/home/user/gridtip/lib/utils/groups.ts` | +| `races.ts` | Race utilities | `/home/user/gridtip/lib/utils/races.ts` | +| `prediction-fields.ts` | Prediction utils | `/home/user/gridtip/lib/utils/prediction-fields.ts` | +| `country-flag.ts` | Flag utilities | `/home/user/gridtip/lib/utils/country-flag.ts` | + +### API Routes + +| File | Purpose | Location | +| ------------------------------ | ------------------- | --------------------------------------------------------- | +| `auth/[...all]/route.ts` | Auth endpoints | `/home/user/gridtip/app/api/auth/[...all]/route.ts` | +| `races/update/route.ts` | Update races | `/home/user/gridtip/app/api/races/update/route.ts` | +| `drivers/update/route.ts` | Update drivers | `/home/user/gridtip/app/api/drivers/update/route.ts` | +| `constructors/update/route.ts` | Update constructors | `/home/user/gridtip/app/api/constructors/update/route.ts` | +| `results/update/route.ts` | Update results | `/home/user/gridtip/app/api/results/update/route.ts` | +| `utils.ts` | API utilities | `/home/user/gridtip/app/api/utils.ts` | + +### Main Pages + +| Route | File Location | +| ----------------------------- | ------------------------------------------------------------- | +| `/` | `/home/user/gridtip/app/(public)/page.tsx` | +| `/auth` | `/home/user/gridtip/app/(standalone)/auth/page.tsx` | +| `/join/[groupId]` | `/home/user/gridtip/app/(standalone)/join/[groupId]/page.tsx` | +| `/tipping` | `/home/user/gridtip/app/tipping/page.tsx` | +| `/tipping/add-tips/[race-id]` | `/home/user/gridtip/app/tipping/add-tips/[race-id]/page.tsx` | +| `/tipping/championships` | `/home/user/gridtip/app/tipping/championships/page.tsx` | +| `/tipping/groups` | `/home/user/gridtip/app/tipping/groups/page.tsx` | +| `/tipping/group-admin` | `/home/user/gridtip/app/tipping/group-admin/page.tsx` | +| `/tipping/leaderboard` | `/home/user/gridtip/app/tipping/leaderboard/page.tsx` | +| `/tipping/settings` | `/home/user/gridtip/app/tipping/settings/page.tsx` | + +### Key Components + +| Component | Location | +| -------------- | -------------------------------------------------- | +| App Sidebar | `/home/user/gridtip/components/app-sidebar.tsx` | +| App Header | `/home/user/gridtip/components/app-header.tsx` | +| Group Switcher | `/home/user/gridtip/components/group-switcher.tsx` | +| Driver Option | `/home/user/gridtip/components/driver-option.tsx` | +| Select Driver | `/home/user/gridtip/components/select-driver.tsx` | +| Breadcrumbs | `/home/user/gridtip/components/breadcrumbs.tsx` | +| UI Components | `/home/user/gridtip/components/ui/*` | + +### Actions + +| Action | Location | +| ------------ | -------------------------------------------- | +| Create Group | `/home/user/gridtip/actions/create-group.ts` | +| Join Group | `/home/user/gridtip/actions/join-group.ts` | +| Edit Group | `/home/user/gridtip/actions/edit-group.ts` | +| Delete User | `/home/user/gridtip/actions/delete-user.ts` | + +### Scripts + +| Script | Location | +| ------------ | -------------------------------------------- | +| Save Avatars | `/home/user/gridtip/scripts/save-avatars.ts` | + +### Tests + +| Test | Location | +| ------------ | --------------------------------------------- | +| Cutoff Tests | `/home/user/gridtip/__tests__/cutoff.test.ts` | + +--- + +## Tips for AI Assistants + +### When Making Changes + +1. **Always check authentication** - Most routes require `verifySession()` +2. **Use proper cache tags** - Revalidate caches when data changes +3. **Follow server/client boundaries** - Mark client components with `'use client'` +4. **Validate with Zod** - All user inputs should be validated +5. **Return ServerResponse** - Server actions should return `{ ok, message }` +6. **Check cutoff logic** - Race tips have time-based cutoffs +7. **Test with sprint races** - Sprint weekends have different prediction fields +8. **Respect admin permissions** - Use `verifyIsAdmin()` for admin-only features + +### When Debugging + +1. **Check middleware** - Authentication issues often stem from middleware +2. **Verify cache tags** - Stale data might be cached +3. **Check database migrations** - Schema changes need migrations +4. **Review Zod schemas** - Validation errors might be from schema mismatches +5. **Check environment variables** - Missing vars cause auth/db failures +6. **Review DAL functions** - Session and permission logic centralized in DAL + +### When Adding Features + +1. **Use existing patterns** - Follow established patterns in codebase +2. **Colocate route-specific code** - Use `_components/`, `actions/`, `_utils/` +3. **Add cache tags** - For data that needs invalidation +4. **Update navigation** - Add to sidebar/breadcrumbs if needed +5. **Write tests** - Add to `__tests__/` directory +6. **Document constants** - Add to `/constants/` if reusable + +--- + +## Related Resources + +- **Next.js Docs**: https://nextjs.org/docs +- **Drizzle ORM**: https://orm.drizzle.team/ +- **better-auth**: https://better-auth.com/ +- **shadcn/ui**: https://ui.shadcn.com/ +- **Jolpica F1 API**: https://github.com/jolpica/jolpica-f1/blob/main/docs/README.md +- **Turso Docs**: https://docs.turso.tech/