another step constructing base project
This commit is contained in:
+122
-1
@@ -39,11 +39,27 @@ client/
|
||||
│ ├── DarkModeButton.tsx # 'use client' — only subscriber to useColorScheme()
|
||||
│ └── index.tsx
|
||||
├── lib/
|
||||
│ ├── api/
|
||||
│ │ ├── client.ts # clientFetch<T> — throws ApiError on error; use in hooks/client components
|
||||
│ │ ├── server.ts # serverFetch<T> — throws ApiError on error; use in RSCs/Server Actions
|
||||
│ │ └── errors.ts # ApiError class (status, message, code)
|
||||
│ ├── query/
|
||||
│ │ ├── queryClient.ts # makeQueryClient factory + getQueryClient() SSR-safe singleton
|
||||
│ │ └── QueryProvider.tsx # 'use client' — QueryClientProvider + ReactQueryDevtools
|
||||
│ └── cookies/ # Cookie manager — strict server/client separation
|
||||
│ ├── constants.ts # COOKIE_NAMES, CookieOptions, COLOR_SCHEME_COOKIE_OPTIONS
|
||||
│ ├── constants.ts # COOKIE_NAMES, CookieOptions, AUTH_*_COOKIE_OPTIONS
|
||||
│ ├── server.ts # getServerCookie, getThemeMode, setServerCookie
|
||||
│ ├── client.ts # getClientCookie, setClientCookie, deleteClientCookie
|
||||
│ └── index.ts # Re-exports constants ONLY (never server/client)
|
||||
├── services/ # Domain services — no top-level barrel; import directly from the file
|
||||
│ └── {domain}/
|
||||
│ ├── types.ts # Request/response types for this domain
|
||||
│ ├── keys.ts # React Query key factory
|
||||
│ ├── apis/
|
||||
│ │ ├── clientApi.ts # Namespace object wrapping clientFetch calls
|
||||
│ │ └── serverApi.ts # Namespace object wrapping serverFetch calls (only when needed)
|
||||
│ └── hooks/
|
||||
│ └── use{Action}.ts # One hook per file — useQuery or useMutation
|
||||
├── store/ # AppStore (Redux-like client state)
|
||||
├── theme/
|
||||
│ ├── ColorSchemeScript.tsx # Inline <script> in <head> — sets attr + patches Storage
|
||||
@@ -284,4 +300,109 @@ Enforcement: before removing or renaming a shared component, check whether `src/
|
||||
- **Do not** import `TYPOGRAPHY` — use `TYPOGRAPHY_LTR` or `TYPOGRAPHY_RTL` explicitly.
|
||||
- **Do not** load fonts inside components or pages — all next/font declarations belong in `src/app/layout.tsx`.
|
||||
- **Do not** import `@/lib/cookies/server` in client components or `@/lib/cookies/client` in RSCs.
|
||||
- **Do not** call `fetch()` directly in components or services — use `serverFetch` (RSC/Server Actions) or `clientFetch` (hooks/Client Components) from `@/lib/api`.
|
||||
- **Do not** create a top-level barrel at `src/services/index.ts` — imports should make the domain origin clear (e.g. `import { useLogin } from '@/services/auth'`, not `import { useLogin } from '@/services'`).
|
||||
- Each domain **does** have an `index.ts` that re-exports its hooks (e.g. `src/services/auth/index.ts`). Do not export `types`, `keys`, or `apis/*` from this barrel — only hooks.
|
||||
- **Do not** mix `clientFetch` and `serverFetch` in the same file — keep `clientApi.ts` and `serverApi.ts` separate; Next.js enforces the environment boundary at build time.
|
||||
- **Do not** toast inside hooks for 401/403/5xx — those are already toasted by `clientFetch`. Only toast in `onError` for domain-specific 4xx messages.
|
||||
- **Do not** call `js-cookie` (`Cookies.*`) directly — use the central client cookie manager (`@/lib/cookies/client`).
|
||||
- **Do not** read or write `document.cookie` directly — use the central client cookie manager.
|
||||
- **Do not** store auth tokens in `sessionStorage` or `localStorage` — use cookies via `@/lib/cookies/client`.
|
||||
|
||||
---
|
||||
|
||||
## API Fetch Services
|
||||
|
||||
Central fetch primitives live in `src/lib/api/`:
|
||||
|
||||
| File | Use from | Purpose |
|
||||
|------|----------|---------|
|
||||
| `client.ts` | hooks, client components | `clientFetch<T>` — throws `ApiError` on error |
|
||||
| `server.ts` | RSCs, Server Actions only | `serverFetch<T>` — throws `ApiError` on error |
|
||||
| `errors.ts` | anywhere | `ApiError` class (`status`, `message`, `code`) |
|
||||
|
||||
**Error contract — `clientFetch`:**
|
||||
- **401** — toast "session expired", clear cookies, redirect to login (no throw; page navigates away)
|
||||
- **403** — toast "forbidden", throw `ApiError`
|
||||
- **5xx** — toast "server error", throw `ApiError`
|
||||
- **Other 4xx** — throw `ApiError`, no toast; the calling hook owns the user-facing message
|
||||
- **Network failure** — toast "network error", throw `ApiError`
|
||||
|
||||
**Error contract — `serverFetch`:**
|
||||
- All errors throw `ApiError` (no toast — server can't fire browser events)
|
||||
- RSC callers decide whether to `notFound()`, `redirect()`, or let the error propagate to an error boundary
|
||||
|
||||
**Domain API calls** live in `src/services/{domain}/apis/clientApi.ts` (or `serverApi.ts`). Never call raw `fetch()` directly.
|
||||
|
||||
---
|
||||
|
||||
## Auth Cookies
|
||||
|
||||
| Cookie | Constant | TTL | Set by |
|
||||
|--------|----------|-----|--------|
|
||||
| `access_token` | `COOKIE_NAMES.ACCESS_TOKEN` | 15 min | `loginUser()` in `src/services/auth.ts` |
|
||||
| `refresh_token` | `COOKIE_NAMES.REFRESH_TOKEN` | 7 days | `loginUser()` in `src/services/auth.ts` |
|
||||
|
||||
Both are regular (non-httpOnly) cookies so they are readable by both server and client.
|
||||
|
||||
**Lifecycle:**
|
||||
- Written after a successful login via `setClientCookie` with `AUTH_ACCESS_COOKIE_OPTIONS` / `AUTH_REFRESH_COOKIE_OPTIONS`
|
||||
- Deleted by `logoutUser()` and automatically by `clientFetch` on 401
|
||||
- Read by `serverFetch` via `getServerCookie(COOKIE_NAMES.ACCESS_TOKEN)`
|
||||
- Read by `clientFetch` via `getClientCookie(COOKIE_NAMES.ACCESS_TOKEN)`
|
||||
|
||||
**Middleware:** validates the `exp` claim of the access token locally (base64-decode the JWT payload, no signature check) before rendering any private page. An absent or expired token redirects to `/{locale}/login`.
|
||||
|
||||
**`useIsAuthenticated()` hook:** SSR-safe — initialises `false`, sets the real value in `useEffect` by reading the cookie. This prevents hydration mismatches.
|
||||
|
||||
---
|
||||
|
||||
## Toast Notifications (notistack)
|
||||
|
||||
`<SnackbarProvider>` wraps all children inside `ThemeProvider` in `src/app/[locale]/layout.tsx`.
|
||||
|
||||
**In React components/hooks** — use notistack directly:
|
||||
```tsx
|
||||
import { useSnackbar } from 'notistack'
|
||||
const { enqueueSnackbar } = useSnackbar()
|
||||
enqueueSnackbar('Saved!', { variant: 'success' })
|
||||
```
|
||||
|
||||
**Outside React** (plain functions, fetch services) — use the event bridge:
|
||||
```ts
|
||||
import { dispatchToast } from '@/lib/toast'
|
||||
dispatchToast('Something went wrong', 'error')
|
||||
```
|
||||
`dispatchToast` fires a `window` CustomEvent (`app:toast`). `ToastBridge` (a zero-UI `'use client'` component inside `SnackbarProvider`) listens and calls `enqueueSnackbar`.
|
||||
|
||||
`ToastBridge` is already rendered in `[locale]/layout.tsx` — do not add another instance.
|
||||
|
||||
---
|
||||
|
||||
## Route Constants
|
||||
|
||||
Named path constants live in `src/constants/routes.ts`:
|
||||
```ts
|
||||
ROUTES.LOGIN = '/login'
|
||||
ROUTES.HOME = '/'
|
||||
PUBLIC_PATHS = [ROUTES.LOGIN, ...] // paths that bypass middleware auth check
|
||||
```
|
||||
Import from the barrel: `import { ROUTES, PUBLIC_PATHS } from '@/constants'`.
|
||||
|
||||
To add a new public route, append it to `PUBLIC_PATHS` — the middleware picks it up automatically.
|
||||
|
||||
---
|
||||
|
||||
## Client Cookie Manager (js-cookie)
|
||||
|
||||
`src/lib/cookies/client.ts` uses `js-cookie` internally. The exported API is unchanged:
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `getClientCookie(name)` | Read a cookie by name |
|
||||
| `setClientCookie(name, value, options?)` | Write a cookie; `options` is `CookieOptions` with `maxAge` in **seconds** |
|
||||
| `deleteClientCookie(name, path?)` | Delete a cookie |
|
||||
| `getColorSchemeCookie()` | Typed helper for the theme cookie |
|
||||
|
||||
`CookieOptions` type is defined in `src/lib/cookies/constants.ts` — `maxAge` is in seconds (converted to `expires: Date` internally when calling js-cookie).
|
||||
- **Do not** `document.title = title` in the render body of any component — it causes `ReferenceError: document is not defined` during build-time prerendering.
|
||||
|
||||
Reference in New Issue
Block a user