From 5f7aed1ae527753b12e5180e52974835a8bb9ac2 Mon Sep 17 00:00:00 2001 From: Nat Silprasert Date: Mon, 14 Sep 2026 13:15:14 +0700 Subject: [PATCH 1/5] refactor: reorganize into feature folders and share auth state via context - Move business components from components/{auth,profile,wallet} into features/{auth,profile,wallet}; components/ now holds only the design system (ui/), route guards (guards/), and app shell (Navbar, Footer). - Rename AuthBoundary to AuthGuard. - Add AuthProvider/useAuth (contexts/auth-context.tsx) as the single source of truth for the signed-in user's profile, replacing independent /users/me and localStorage reads in AuthGuard, Navbar, and ProfilePanel. - Update AGENTS.md and README.md to document the new structure. --- AGENTS.md | 7 + README.md | 7 + apps/web/src/app/balance/top-up/page.tsx | 2 +- apps/web/src/app/complete-profile/page.tsx | 2 +- apps/web/src/app/layout.tsx | 9 +- apps/web/src/app/login/page.tsx | 2 +- apps/web/src/app/profile/edit/page.tsx | 2 +- apps/web/src/app/profile/page.tsx | 2 +- apps/web/src/app/register/page.tsx | 2 +- apps/web/src/components/Navbar.tsx | 54 +---- .../web/src/components/auth/auth-boundary.tsx | 151 -------------- apps/web/src/components/guards/auth-guard.tsx | 113 +++++++++++ .../src/components/profile/profile-panel.tsx | 190 ------------------ apps/web/src/contexts/auth-context.tsx | 107 ++++++++++ .../auth/login-form.stories.tsx | 0 .../auth/login-form.tsx | 0 .../auth/login-panel.tsx | 2 +- .../auth/registration-form.stories.tsx | 0 .../auth/registration-form.tsx | 0 .../auth/registration-panel.tsx | 2 +- .../profile/profile-avatar.tsx | 0 .../profile/profile-completion-form.tsx | 0 .../profile/profile-details.stories.tsx | 0 .../profile/profile-details.tsx | 2 +- .../profile/profile-edit-form.tsx | 5 +- .../src/features/profile/profile-panel.tsx | 72 +++++++ .../wallet/top-up-form.tsx | 4 +- 27 files changed, 336 insertions(+), 401 deletions(-) delete mode 100644 apps/web/src/components/auth/auth-boundary.tsx create mode 100644 apps/web/src/components/guards/auth-guard.tsx delete mode 100644 apps/web/src/components/profile/profile-panel.tsx create mode 100644 apps/web/src/contexts/auth-context.tsx rename apps/web/src/{components => features}/auth/login-form.stories.tsx (100%) rename apps/web/src/{components => features}/auth/login-form.tsx (100%) rename apps/web/src/{components => features}/auth/login-panel.tsx (92%) rename apps/web/src/{components => features}/auth/registration-form.stories.tsx (100%) rename apps/web/src/{components => features}/auth/registration-form.tsx (100%) rename apps/web/src/{components => features}/auth/registration-panel.tsx (95%) rename apps/web/src/{components => features}/profile/profile-avatar.tsx (100%) rename apps/web/src/{components => features}/profile/profile-completion-form.tsx (100%) rename apps/web/src/{components => features}/profile/profile-details.stories.tsx (100%) rename apps/web/src/{components => features}/profile/profile-details.tsx (99%) rename apps/web/src/{components => features}/profile/profile-edit-form.tsx (98%) create mode 100644 apps/web/src/features/profile/profile-panel.tsx rename apps/web/src/{components => features}/wallet/top-up-form.tsx (97%) diff --git a/AGENTS.md b/AGENTS.md index 4f0f467..e83c178 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,16 @@ Yarn workspaces monorepo, two independent apps talking only over HTTP: - `apps/api` — Express + Prisma/Postgres, layered `router/ → controller/ → service/ → repository/` per resource (see `task.*.ts` for the pattern). Validation is zod in the controller; no DTO layer. Every response is `{ success, message, data }`, thrown via `common/errors/app-error.ts` typed errors and centralized in `common/middleware/error-handler.middleware.ts`. - `apps/web` — Next.js App Router. Server Components fetch data (`lib/api-client.ts`, which unwraps the `{ success, data }` envelope); anything interactive needs its own `"use client"` component, and mutations should call `router.refresh()` afterward since there's no client cache. + - `src/features//` — business-specific components grouped by feature (`auth`, `profile`, `wallet`); e.g. `features/auth/login-panel.tsx` wires the presentational form to the API and router. + - `src/components/ui/` — base/design-system primitives (button, input, select, ...), documented in `DESIGN.md`. + - `src/components/guards/auth-guard.tsx` (`AuthGuard`) — the single client-side route guard, mounted once in `app/layout.tsx`. Matches the path against a `ROUTE_RULES` table (guest-only / profile-completion-required / role-restricted per prefix) and redirects before rendering — add a route there instead of a new `layout.tsx`. + - `src/components/Navbar.tsx`, `Footer.tsx` — global app shell, not tied to one feature. + - `src/contexts/auth-context.tsx` — `AuthProvider` + `useAuth()`, the single source of truth for the signed-in user's profile (`GET /users/me`). `AuthGuard`, `Navbar`, and profile/wallet features read from it instead of each fetching independently; call `refresh()` after a mutation that can change role/profile-completion/wallet state. ## Conventions - New API resource: add one file each to `router/`, `controller/`, `service/`, `repository/`, then register in `apps/api/src/routes.ts`. - New shared frontend type: one file per resource under `apps/web/src/types/`. +- New frontend feature UI goes under `apps/web/src/features//`, not `components/`; `components/` is reserved for the design system (`ui/`), route guards (`guards/`), and app-shell pieces (`Navbar`, `Footer`). +- New protected/guest page: add a prefix to `ROUTE_RULES` in `apps/web/src/components/guards/auth-guard.tsx`. - Formatting is enforced by Prettier (100 cols, double quotes, semicolons) + ESLint, run automatically on commit via husky/lint-staged — don't bypass with `--no-verify`. diff --git a/README.md b/README.md index 90a0b2d..8918d53 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,13 @@ Monorepo: Next.js frontend + Express API, Prisma/Postgres, Swagger docs, Storybo ## Stack - **apps/web** — Next.js, Tailwind CSS, shadcn/ui, lucide-react, Storybook + - `src/features//` — feature UI grouped by business domain (`auth`, + `profile`, `wallet`) + - `src/components/ui/` — base design-system primitives, see `DESIGN.md` + - `src/components/guards/auth-guard.tsx` — the single route guard mounted in + `app/layout.tsx`; redirects per-route based on sign-in/profile-completion/role + - `src/contexts/auth-context.tsx` — `AuthProvider`/`useAuth()`, the shared + signed-in user profile consumed by the guard, navbar, and feature components - **apps/api** — Express, Prisma, PostgreSQL, Swagger (`swagger-jsdoc` + `swagger-ui-express`) - Layers: `router/` → `controller/` → `service/` → `repository/`. Validation (zod) lives in the controller; no separate DTO layer. diff --git a/apps/web/src/app/balance/top-up/page.tsx b/apps/web/src/app/balance/top-up/page.tsx index 8c3425b..d397380 100644 --- a/apps/web/src/app/balance/top-up/page.tsx +++ b/apps/web/src/app/balance/top-up/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import { TopUpForm } from "@/components/wallet/top-up-form"; +import { TopUpForm } from "@/features/wallet/top-up-form"; export const metadata: Metadata = { title: "Top up balance | Tutorist" }; diff --git a/apps/web/src/app/complete-profile/page.tsx b/apps/web/src/app/complete-profile/page.tsx index ff1da77..f5e1439 100644 --- a/apps/web/src/app/complete-profile/page.tsx +++ b/apps/web/src/app/complete-profile/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; -import { ProfileCompletionForm } from "@/components/profile/profile-completion-form"; +import { ProfileCompletionForm } from "@/features/profile/profile-completion-form"; export const metadata: Metadata = { title: "Complete your profile | Tutorist", diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 6710bd0..bb56d49 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,8 +1,9 @@ import type { Metadata } from "next"; import "./globals.css"; -import { AuthBoundary } from "@/components/auth/auth-boundary"; +import { AuthGuard } from "@/components/guards/auth-guard"; import { Footer } from "@/components/Footer"; import { Navbar } from "@/components/Navbar"; +import { AuthProvider } from "@/contexts/auth-context"; export const metadata: Metadata = { title: "SE Project", @@ -13,8 +14,10 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( - - {children} + + + {children} +