# AGENTS.md — Pram Project Guide > This file is for AI agents. Read this first before making any changes. --- ## 1. Project Identity - **Name:** `pram` — AI-powered multi-language test practice platform. - **Repo type:** Turborepo monorepo (Bun workspaces). - **Workspace packages:** `apps/*`, `packages/*`. - **Runtime & package manager:** **Bun** (`packageManager: "bun@1.3.11"`). - Do **NOT** use `pnpm`, `npm`, or `yarn` unless explicitly required by a global tool. --- ## 2. Technology Stack | Layer | Technology | Notes | |-------|------------|-------| | Frontend | React 19 + Vite + TanStack Router | File-based routing. **Not Next.js.** | | Backend | Hono + tRPC | Entry: `apps/server/src/index.ts`. Port 3000. | | DB Engine | PostgreSQL | Managed via Drizzle. | | DB ORM | Drizzle ORM | **Not Prisma.** | | Auth | Better-Auth | Email/password + OAuth. | | UI | shadcn/ui (shared) | Lives in `packages/ui`. Imported as `@pram/ui/components/*`. | | AI | OpenAI-compatible API | User-managed API keys via Settings UI. | | Build | Turborepo + `tsdown` | Server compiled with `tsdown`. | --- ## 3. Architecture Overview ``` pram/ ├── apps/ │ ├── web/ # Frontend (Vite, TanStack Router) — Port 5173 │ └── server/ # Backend (Hono, tRPC) — Port 3000 ├── packages/ │ ├── ui/ # Shared shadcn/ui components & styles │ ├── api/ # tRPC routers & business logic │ ├── auth/ # Better-Auth configuration │ ├── db/ # Drizzle schema, queries, migrations │ ├── ai/ # AI generation: prompts, schemas, agentic pipeline │ ├── env/ # Shared environment validation │ └── config/ # Shared TypeScript configs ``` - **Internal imports** use the `@pram/*` workspace namespace. - Do **NOT** create cross-imports between `apps/*` directly; route through `packages/*`. --- ## 4. Essential Commands Use these exact commands. Do not substitute with `pnpm`/`npm`/`npx` equivalents. ```bash # Install dependencies bun install # Development bun run dev # Start all apps (web + server) bun run dev:web # Start web app only bun run dev:server # Start server only # Type checking bun run check-types # Check all packages # Database (Drizzle) bun run db:push # Push schema changes to PostgreSQL bun run db:studio # Open Drizzle Studio UI bun run db:migrate # Run migrations bun run db:generate # Generate migration files bun run db:seed # Seed reference data (exam types, etc.) bun run db:start # Start local DB (if configured) bun run db:stop # Stop local DB # Testing bun test # Run all tests bun run turbo test # Run via turbo pipeline # Build bun run build # Build all packages ``` --- ## 5. Common Pitfalls — DO NOT - ❌ **Do not use `pnpm` / `npm` / `yarn`** for package management or running scripts. - ❌ **Do not assume Docker or Docker Compose** exists in this repo. There are no Dockerfiles or compose files. - ❌ **Do not assume Next.js**. This project uses **Vite + TanStack Router**, not Next.js App Router or Pages Router. - ❌ **Do not assume Prisma**. Database layer is **Drizzle ORM**. - ❌ **Do not hardcode API keys** in source code. AI keys are user-managed via the Settings UI. - ❌ **Do not add global CSS** in `apps/web` without checking `packages/ui/src/styles/globals.css` for existing design tokens and CSS variables. - ❌ **Do not skip heading levels**. After `

`, the next heading must be `

`, not `

` (or lower). Never have two `

` on one page. - ❌ **Do not create custom modal/dialog components**. Use shadcn `` from `@pram/ui/components/dialog` — it provides built-in focus trap, escape key, scroll lock, and ARIA. - ❌ **Do not use `any`** in frontend TypeScript. Enforced by eslint `@typescript-eslint/no-explicit-any: "error"` in `apps/web/eslint.config.mjs`. --- ## 6. UI / Styling Guidelines - Design tokens and global styles live in **`packages/ui/src/styles/globals.css`**. - shadcn/ui primitives are shared via **`packages/ui`**. - Import components like this: ```tsx import { Button } from "@pram/ui/components/button"; ``` - To add new shadcn primitives, run from the **project root**: ```bash npx shadcn@latest add accordion dialog -c packages/ui ``` - App-specific blocks (non-shared) should be added directly in `apps/web/src/components/`. - DevTools (TanStack Router, React Query) are **production-gated** via `import.meta.env.DEV` — do not remove the condition. - App-level error boundary is in `__root.tsx` using `` — route-level error boundaries via `errorComponent` route option. - Lazy-loaded route components use the `.lazy.tsx` pattern: route file is thin, heavy component lives in `components/routes/*Page.tsx` wrapped in `Suspense`. ### Sticky Detail Page Header Pattern Detail pages with long scrollable content (e.g. `package.$id.index.tsx`, `attempt.$id.tsx`) use a **sticky top header** pattern: ```tsx
{/* Sticky header */}
{/* Compact breadcrumb — text-xs, truncated title */} {/* Title row (left) + action buttons (right) */} {/* Stats row — text-xs */}
{/* Scrollable content */}
...
``` Rules: - Primary CTA ("Mulai Latihan", "Coba Lagi") and secondary actions (Edit, Share) live in the sticky header — **never only at the bottom** of a long page. - Breadcrumb inside the header is `text-xs` and truncates the current page title to `max-w-[240px]`. - On small screens (`sm:` breakpoint), button text labels hide (`hidden sm:inline`) leaving only icons. ### Public/Private Toggle Hover Pattern Any button that toggles visibility between public and private must reveal **intent on hover** (what will happen, not what it currently is): ```tsx ``` This pattern is used in `QuestionCard.tsx` and `PackageCard.tsx`. ### Component Folder Conventions Large "card" components for list items are extracted into their own folder and file, **not** inlined inside the page component: | Domain | Card component | Used in | |--------|---------------|---------| | Questions | `apps/web/src/components/bank/QuestionCard.tsx` | `BankPage.tsx` | | Packages | `apps/web/src/components/packages/PackageCard.tsx` | `PackagesPage.tsx` | When a list item card grows beyond ~40 lines of JSX, extract it. Pass all state through props; the card must be stateless (no data fetching). ### CalloutCard `apps/web/src/components/bank/CalloutCard.tsx` is a reusable banner for "N items are private, publish them all" prompts. Use the `label` prop to customise the noun: ```tsx ``` `count` should come from a **dedicated query with `limit: 1`** (fetches only the `total` field), **not** from the locally accumulated infinite-scroll array — those counts are partial and misleading. --- ## 5b. Route Shell & Layout Modes ### Route Shell System Every route declares its layout via `staticData` from `apps/web/src/lib/route-shell.ts`. The root layout (`__root.tsx`) reads this to decide which shell to render: | Shell | When to use | What renders | |-------|-------------|-------------| | `routeShell.app` | Authenticated app pages | Sidebar + `
` + SkipLink | | `routeShell.public` | Landing, login, auth flows, 404 | Bare `min-h-screen` wrapper, no sidebar | | `routeShell.fullscreen` | Test-taking, immersive UIs | `h-screen overflow-y-auto`, no sidebar | Usage in a route file: ```tsx import { routeShell } from "@/lib/route-shell"; export const Route = createFileRoute("/my-route")({ staticData: routeShell.app, // or .public / .fullscreen ... }); ``` **Rules:** - **Always** set `staticData` on every route. Routes without it default to `"app"` shell (sidebar shown). - Auth routes (`/login`, `/forgot-password`, `/verify-email`, `/setup-avatar`) use `routeShell.public`. - Test routes (`/package/$id/take`, `/package/$id/attempt/$attemptId`) use `routeShell.fullscreen`. - Admin routes (`/admin`, `/admin/*`) use `routeShell.fullscreen`. ### Route Structure Conventions - **`/`** — Public landing page (`index.tsx` + `index.lazy.tsx`). Redirects logged-in users to `/dashboard`. - **`/dashboard`** — Authenticated home page (`dashboard.tsx`). Redirects unauthenticated users to `/login`. - **`/landing`** — Legacy redirect → `/` (keep for backward compat, do not remove). - **`/$.tsx`** — Catch-all 404 route. Uses `routeShell.public` so no sidebar appears. Renders ``. - **Do NOT** put a `notFoundComponent` in `__root.tsx`. The 404 is handled exclusively by the splat route `$.tsx`. --- ## 5c. Frontend Accessibility Conventions - **Skip link**: Root layout in `__root.tsx` has a hidden `` that appears on Tab press, linking to `#main-content` on the `
` element. - **Icon-only buttons**: Every `