doo-boilerplate 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ function validateProjectName(name) {
28
28
  async function collectOptions(defaults, isTTY2 = true) {
29
29
  if (!isTTY2) {
30
30
  const valid = ["editor", "charts", "dnd", "sentry"];
31
- const validPMs2 = ["npm", "yarn", "pnpm", "bun", "deno"];
31
+ const validPMs2 = ["pnpm", "bun", "yarn"];
32
32
  const projectName2 = defaults.projectName ?? "my-portal";
33
33
  const framework2 = defaults.framework === "nextjs" || defaults.framework === "vite" ? defaults.framework : "vite";
34
34
  const packageManager2 = validPMs2.includes(defaults.pm) ? defaults.pm : "pnpm";
@@ -64,8 +64,8 @@ async function collectOptions(defaults, isTTY2 = true) {
64
64
  const answer = await p.select({
65
65
  message: "Framework",
66
66
  options: [
67
- { value: "nextjs", label: "Next.js 16", hint: "SSR \xB7 App Router \xB7 i18n-ready" },
68
- { value: "vite", label: "Vite 7", hint: "SPA \xB7 faster builds \xB7 TanStack Router" }
67
+ { value: "vite", label: "Vite 8", hint: "SPA \xB7 faster builds \xB7 TanStack Router" },
68
+ { value: "nextjs", label: "Next.js 16", hint: "SSR \xB7 App Router \xB7 i18n-ready" }
69
69
  ]
70
70
  });
71
71
  if (p.isCancel(answer)) {
@@ -74,7 +74,7 @@ async function collectOptions(defaults, isTTY2 = true) {
74
74
  }
75
75
  framework = answer;
76
76
  }
77
- const validPMs = ["npm", "yarn", "pnpm", "bun", "deno"];
77
+ const validPMs = ["pnpm", "bun", "yarn"];
78
78
  let packageManager;
79
79
  if (validPMs.includes(defaults.pm)) {
80
80
  packageManager = defaults.pm;
@@ -82,11 +82,9 @@ async function collectOptions(defaults, isTTY2 = true) {
82
82
  const answer = await p.select({
83
83
  message: "Package manager",
84
84
  options: [
85
- { value: "npm", label: "npm", hint: "Node package manager" },
86
- { value: "yarn", label: "yarn", hint: "Fast, reliable" },
87
- { value: "pnpm", label: "pnpm", hint: "Efficient disk usage" },
88
- { value: "bun", label: "bun", hint: "All-in-one runtime" },
89
- { value: "deno", label: "deno", hint: "Secure by default" }
85
+ { value: "pnpm", label: "pnpm", hint: "recommended \xB7 efficient disk usage" },
86
+ { value: "bun", label: "bun", hint: "fastest \xB7 all-in-one runtime" },
87
+ { value: "yarn", label: "yarn", hint: "reliable \xB7 stable" }
90
88
  ]
91
89
  });
92
90
  if (p.isCancel(answer)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doo-boilerplate",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI to scaffold Pila portal frontend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,215 +1,297 @@
1
1
  # {{PROJECT_NAME}}
2
2
 
3
- > Next.js 16 server-side rendered portal frontend
4
-
5
- Production-ready Next.js 16 application with App Router, i18n, dark mode, authentication, and DevOps integration.
3
+ > Next.js 16 SSR portal — scaffolded by [doo-boilerplate](https://www.npmjs.com/package/doo-boilerplate)
6
4
 
7
5
  ## Quick Start
8
6
 
9
7
  ```bash
10
- pnpm dev
8
+ cp .env.example .env # configure API URLs
9
+ pnpm dev # http://localhost:3000
11
10
  ```
12
11
 
13
- Open [http://localhost:3000](http://localhost:3000) in your browser.
12
+ ---
14
13
 
15
- ## Available Scripts
14
+ ## Scripts
16
15
 
17
16
  | Command | Purpose |
18
17
  |---------|---------|
19
- | `pnpm dev` | Start dev server with Turbopack |
20
- | `pnpm build` | Build for production |
18
+ | `pnpm dev` | Dev server with Turbopack |
19
+ | `pnpm build` | Production build |
21
20
  | `pnpm start` | Run production server |
22
- | `pnpm lint` | Run ESLint |
23
- | `pnpm lint:fix` | Fix linting issues |
24
- | `pnpm type-check` | TypeScript type checking |
25
- | `pnpm format` | Format code with Prettier |
26
- | `pnpm format:check` | Check formatting without changes |
27
- | `pnpm knip` | Find unused files/exports |
28
- | `pnpm gen:api` | Generate types from Swagger spec |
29
- | `pnpm gen:api:watch` | Watch mode for API generation |
30
- | `pnpm docker:build` | Build & scan Docker image |
31
- | `pnpm docker:scan` | Security scan image with Trivy |
32
-
33
- ## Tech Stack
34
-
35
- - **Framework:** Next.js 16 with App Router
36
- - **Styling:** Tailwind CSS 4 + Shadcn/ui
37
- - **i18n:** next-intl (EN/VI built-in)
38
- - **State:** Zustand 5 (global) + TanStack Query 5 (server)
39
- - **Forms:** React Hook Form + Zod
40
- - **HTTP:** Axios with JWT interceptors
41
- - **Auth:** JWT store with refresh token persistence
42
- - **DevOps:** Docker (multi-stage) + Trivy security scan
43
-
44
- ## Environment Variables
45
-
46
- Copy `.env.example` to `.env`:
21
+ | `pnpm lint` / `lint:fix` | ESLint |
22
+ | `pnpm type-check` | TypeScript |
23
+ | `pnpm format` | Prettier |
24
+ | `pnpm knip` | Find unused code |
25
+ | `pnpm gen:api` | Generate types from `docs/swagger/api.json` |
26
+ | `pnpm gen:api:watch` | Watch mode |
27
+ | `pnpm docker:build` | Build image + Trivy security scan |
28
+ | `pnpm docker:scan` | Scan existing image |
47
29
 
48
- ```bash
49
- cp .env.example .env
50
- ```
51
-
52
- Key variables:
53
-
54
- ```env
55
- # API Configuration
56
- NEXT_PUBLIC_API_URL=http://localhost:8000
57
- NEXT_PUBLIC_API_AUTH_URL=http://localhost:8001
58
-
59
- # App
60
- NEXT_PUBLIC_APP_URL=http://localhost:3000
61
- NEXT_PUBLIC_APP_NAME={{PROJECT_NAME}}
62
- ```
30
+ ---
63
31
 
64
- ## Project Structure
32
+ ## Folder Structure
65
33
 
66
34
  ```
67
35
  src/
68
- ├── app/
69
- │ ├── (auth)/ # Auth layout group (login, register)
70
- │ ├── (dashboard)/ # Protected layout group
71
- ├── layout.tsx # Root layout
72
- └── page.tsx # Homepage
36
+ ├── app/ # Next.js App Router (pages + layouts)
37
+ │ ├── (auth)/ # Unauthenticated route group
38
+ ├── layout.tsx # Minimal layout (no sidebar)
39
+ │ └── sign-in/page.tsx
40
+ ├── (dashboard)/ # Protected route group
41
+ │ │ ├── layout.tsx # Full layout: sidebar + header
42
+ │ │ ├── page.tsx # /dashboard
43
+ │ │ ├── profile/page.tsx # /profile
44
+ │ │ └── settings/page.tsx # /settings
45
+ │ ├── api/health/route.ts # Health check: GET /api/health
46
+ │ ├── layout.tsx # Root: fonts, providers, html lang
47
+ │ └── not-found.tsx # 404 page
48
+
73
49
  ├── components/
74
- │ ├── ui/ # Shadcn/ui components
75
- │ └── **/ # Feature-specific components
76
- ├── features/
77
- │ ├── auth/
78
- │ │ ├── hooks/
79
- │ │ ├── stores/ # Zustand auth store
80
- │ ├── services/
81
- │ │ └── gen/ # Generated API types (gen:api)
82
- │ └── types/
83
- └── **/
84
- ├── hooks/ # Custom React hooks
50
+ │ ├── ui/ # Shadcn/ui primitives — DO NOT EDIT
51
+ └── button, card, dialog, form, input, select, ...
52
+ ├── common/ # Shared app-wide components
53
+ ├── error-boundary.tsx
54
+ │ │ ├── loading-spinner.tsx
55
+ │ │ └── theme-toggle.tsx # Dark/light/system toggle
56
+ └── layout/ # Page chrome
57
+ ├── sidebar.tsx # Collapsible nav sidebar
58
+ ├── header.tsx # Top bar with user menu
59
+ └── page-layout.tsx # Wrapper: sidebar + main content
60
+
61
+ ├── features/ # Domain modules (feature-first)
62
+ │ └── auth/
63
+ │ ├── components/
64
+ │ │ └── sign-in-form.tsx
65
+ │ ├── hooks/
66
+ │ │ └── use-auth.ts # useSignIn, useSignOut, useCurrentUser
67
+ │ ├── schemas/
68
+ │ │ └── auth-schema.ts # Zod: LoginSchema, RegisterSchema
69
+ │ └── services/
70
+ │ ├── auth-api.ts # API calls: login, logout, me
71
+ │ └── gen/ # Auto-generated types (pnpm gen:api)
72
+
73
+ ├── hooks/ # Shared custom hooks
74
+ │ └── use-media-query.ts
75
+
85
76
  ├── i18n/
86
- ├── request.ts # Server-side i18n setup
87
- ├── routing.ts # Route configuration
88
- │ └── translations/ # Message files
89
- ├── lib/
90
- │ ├── cn.ts # Tailwind merge utility
91
- ├── http.ts # Axios instance
92
- └── **/
93
- ├── middleware.ts # next-intl middleware
77
+ └── config.ts # locales: ['en', 'vi'], defaultLocale: 'en'
78
+
79
+ ├── lib/ # Utilities & singletons
80
+ ├── api-client.ts # Axios instance (auto Bearer token + 401 handler)
81
+ │ ├── query-client.ts # TanStack Query client config
82
+ └── utils.ts # cn() = clsx + tailwind-merge
83
+
84
+ ├── middleware.ts # next-intl routing (locale prefix)
85
+
94
86
  ├── providers/
95
- │ ├── ClientProvider.tsx # Client-side providers
96
- └── ThemeProvider.tsx # next-themes setup
97
- ├── stores/ # Zustand stores
87
+ │ ├── app-providers.tsx # Compose all providers here
88
+ ├── query-provider.tsx # <QueryClientProvider>
89
+ │ └── theme-provider.tsx # <ThemeProvider> (next-themes)
90
+
91
+ ├── stores/
92
+ │ └── auth-store.ts # Zustand auth (persisted to localStorage)
93
+
98
94
  ├── styles/
99
- ├── globals.css
100
- └── variables.css # CSS variables
95
+ └── globals.css # @import tailwindcss + CSS variables
96
+
101
97
  └── types/
102
- └── index.ts
98
+ └── index.ts # Shared TS types (ApiError, PaginatedResponse, etc.)
103
99
  ```
104
100
 
105
- ## API Code Generation
101
+ ---
106
102
 
107
- Generate TypeScript types from backend Swagger spec:
103
+ ## Design Patterns
108
104
 
109
- ```bash
110
- # Place spec at docs/swagger/api.json
111
- curl https://api.example.com/swagger.json > docs/swagger/api.json
105
+ ### Feature-First Modules
112
106
 
113
- # Generate types
114
- pnpm gen:api
107
+ Add new domain logic as a feature module:
115
108
 
116
- # Watch mode during API development
117
- pnpm gen:api:watch
109
+ ```
110
+ features/users/
111
+ ├── components/user-table.tsx
112
+ ├── hooks/use-users.ts ← TanStack Query hooks
113
+ ├── schemas/user-schema.ts ← Zod validation
114
+ └── services/
115
+ ├── users-api.ts ← API calls
116
+ └── gen/ ← Generated types (optional)
118
117
  ```
119
118
 
120
- Generated types appear in `src/features/auth/services/gen/`.
119
+ ### State Layers
121
120
 
122
- ## Docker Deployment
121
+ | State type | Tool | Location |
122
+ |-----------|------|----------|
123
+ | Server / async data | TanStack Query | `features/{x}/hooks/` |
124
+ | Global client state | Zustand | `src/stores/` |
125
+ | Form state | React Hook Form | component-local |
126
+ | URL / route params | `useSearchParams` | component-local |
123
127
 
124
- ### Build & Scan
128
+ **Rule:** Never put fetched data in Zustand. Only put truly global client state (auth, UI prefs) in stores.
125
129
 
126
- ```bash
127
- pnpm docker:build
128
- ```
130
+ ### Auth Store
129
131
 
130
- Builds optimized multi-stage image and scans for vulnerabilities with Trivy.
132
+ ```ts
133
+ import { useAuthStore } from '@/stores/auth-store'
131
134
 
132
- ### Local Development
135
+ // Read
136
+ const { user, isAuthenticated } = useAuthStore()
133
137
 
134
- ```bash
135
- docker-compose up
136
- # App runs on http://localhost:3000
138
+ // RBAC
139
+ const isAdmin = useAuthStore(s => s.hasRole('admin'))
140
+ const canPublish = useAuthStore(s => s.hasPermission('publish:articles'))
141
+
142
+ // Mutations
143
+ const { login, logout } = useAuthStore()
137
144
  ```
138
145
 
139
- Environment variables read from `.env` file.
146
+ Persisted fields: `user`, `accessToken`, `refreshToken`, `isAuthenticated`.
140
147
 
141
- ### Production Build
148
+ ### API Client
142
149
 
143
- ```bash
144
- docker build -t my-app:latest \
145
- --build-arg NEXT_PUBLIC_API_URL=https://api.example.com .
150
+ ```ts
151
+ // All features use the shared client from lib/api-client.ts
152
+ import apiClient from '@/lib/api-client'
146
153
 
147
- docker run -p 3000:3000 \
148
- -e NEXT_PUBLIC_API_URL=https://api.example.com \
149
- my-app:latest
154
+ // features/users/services/users-api.ts
155
+ export const usersApi = {
156
+ list: (params?: ListParams) =>
157
+ apiClient.get<User[]>('/users', { params }).then(r => r.data),
158
+ create: (data: CreateUserDto) =>
159
+ apiClient.post<User>('/users', data).then(r => r.data),
160
+ update: (id: string, data: UpdateUserDto) =>
161
+ apiClient.patch<User>(`/users/${id}`, data).then(r => r.data),
162
+ }
150
163
  ```
151
164
 
152
- ## Next.js App Router Basics
165
+ The client auto-injects `Authorization: Bearer <token>` and clears auth + redirects to `/sign-in` on 401.
153
166
 
154
- ### Routing
167
+ ### TanStack Query Hooks
155
168
 
156
- Routes defined by file structure in `src/app/`:
169
+ ```ts
170
+ // features/users/hooks/use-users.ts
171
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
172
+ import { usersApi } from '../services/users-api'
157
173
 
158
- ```
159
- app/
160
- ├── page.tsx → /
161
- ├── about/
162
- │ └── page.tsx → /about
163
- └── (auth)/
164
- ├── login/
165
- │ └── page.tsx → /login
166
- └── register/
167
- └── page.tsx → /register
174
+ export function useUsers(params?: ListParams) {
175
+ return useQuery({
176
+ queryKey: ['users', params],
177
+ queryFn: () => usersApi.list(params),
178
+ staleTime: 5 * 60_000,
179
+ })
180
+ }
181
+
182
+ export function useCreateUser() {
183
+ const qc = useQueryClient()
184
+ return useMutation({
185
+ mutationFn: usersApi.create,
186
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),
187
+ })
188
+ }
168
189
  ```
169
190
 
170
- Parentheses `(auth)` create layout groups without affecting URL.
191
+ ### Form Pattern
171
192
 
172
- ### Server & Client Components
193
+ ```ts
194
+ import { useForm } from 'react-hook-form'
195
+ import { zodResolver } from '@hookform/resolvers/zod'
196
+ import { z } from 'zod'
173
197
 
174
- By default, all components are **Server Components** (run on server):
198
+ const schema = z.object({ name: z.string().min(2) })
199
+ type Values = z.infer<typeof schema>
175
200
 
176
- ```typescript
177
- // src/app/page.tsx (Server Component)
178
- export default function Home() {
179
- return <h1>SSR rendered on server</h1>
201
+ function CreateUserForm() {
202
+ const form = useForm<Values>({ resolver: zodResolver(schema) })
203
+ const { mutate, isPending } = useCreateUser()
204
+
205
+ return (
206
+ <Form {...form}>
207
+ <form onSubmit={form.handleSubmit(data => mutate(data))}>
208
+ <FormField control={form.control} name="name" render={...} />
209
+ <Button disabled={isPending}>Create</Button>
210
+ </form>
211
+ </Form>
212
+ )
180
213
  }
181
214
  ```
182
215
 
183
- For client-side logic, add `'use client'`:
216
+ ### Protected Routes
184
217
 
185
- ```typescript
186
- // src/components/Counter.tsx (Client Component)
187
- 'use client'
218
+ Route groups handle protection at the layout level:
188
219
 
189
- import { useState } from 'react'
220
+ ```ts
221
+ // app/(dashboard)/layout.tsx
222
+ import { redirect } from 'next/navigation'
223
+ import { getAuthFromCookies } from '@/lib/auth-server'
190
224
 
191
- export function Counter() {
192
- const [count, setCount] = useState(0)
193
- return <button onClick={() => setCount(count + 1)}>{count}</button>
225
+ export default async function DashboardLayout({ children }) {
226
+ const auth = await getAuthFromCookies()
227
+ if (!auth?.isAuthenticated) redirect('/sign-in')
228
+ return <PageLayout>{children}</PageLayout>
194
229
  }
195
230
  ```
196
231
 
197
- ### Layouts
232
+ ### Internationalization
198
233
 
199
- Create shared layouts for route groups:
234
+ ```ts
235
+ // Server Component
236
+ import { getTranslations } from 'next-intl/server'
237
+ const t = await getTranslations('dashboard')
200
238
 
201
- ```typescript
202
- // src/app/(dashboard)/layout.tsx
203
- export default function DashboardLayout({ children }) {
204
- return (
205
- <div>
206
- <nav>Sidebar</nav>
207
- <main>{children}</main>
208
- </div>
209
- )
210
- }
239
+ // Client Component ('use client')
240
+ import { useTranslations } from 'next-intl'
241
+ const t = useTranslations('dashboard')
242
+
243
+ t('welcome') // → "Welcome" / "Chào mừng"
244
+ t('items', { n: 5 }) // with interpolation
211
245
  ```
212
246
 
247
+ Add translations in `messages/en.json` and `messages/vi.json`.
248
+
249
+ ---
250
+
251
+ ## API Type Generation
252
+
253
+ ```bash
254
+ # 1. Place your backend OpenAPI spec
255
+ curl https://api.example.com/swagger.json > docs/swagger/api.json
256
+
257
+ # 2. Generate TypeScript types
258
+ pnpm gen:api
259
+
260
+ # 3. Import generated types
261
+ import type { LoginRequest } from '@/features/auth/services/gen'
262
+ ```
263
+
264
+ To generate types for a different feature, update the `-o` path in `package.json`:
265
+
266
+ ```json
267
+ "gen:api": "swagger-typescript-api -p ./docs/swagger/api.json -o ./src/features/users/services/gen --no-client --modular"
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Environment Variables
273
+
274
+ ```env
275
+ # API
276
+ NEXT_PUBLIC_API_URL=http://localhost:8000
277
+ NEXT_PUBLIC_API_AUTH_URL=http://localhost:8001
278
+
279
+ # App
280
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
281
+ NEXT_PUBLIC_APP_NAME={{PROJECT_NAME}}
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Docker
287
+
288
+ ```bash
289
+ pnpm docker:build # Build + Trivy scan
290
+ docker-compose up # Local dev stack on :3000
291
+ ```
292
+
293
+ The Dockerfile uses 3 stages: `deps` → `builder` → `runner` (minimal Node image). Uses `output: 'standalone'` for smallest container size.
294
+
213
295
  ---
214
296
 
215
- **Learn more:** [Next.js Docs](https://nextjs.org/docs)
297
+ **Framework:** Next.js 16 · **Styling:** Tailwind CSS 4 + Shadcn/ui · **i18n:** next-intl (EN/VI) · **State:** Zustand 5 + TanStack Query 5
@@ -40,7 +40,7 @@ export function SignInForm() {
40
40
  <Card className="w-full max-w-md">
41
41
  <CardHeader className="space-y-1">
42
42
  <CardTitle className="text-2xl font-bold">Sign in</CardTitle>
43
- <CardDescription>Enter your credentials to access your account</CardDescription>
43
+ <CardDescription>Enter your credentials to access your account · or try <strong>demo / demo</strong></CardDescription>
44
44
  </CardHeader>
45
45
  <CardContent>
46
46
  <Form {...form}>
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  export const signInSchema = z.object({
4
- email: z.string().email('Invalid email address'),
5
- password: z.string().min(6, 'Password must be at least 6 characters'),
4
+ email: z.union([z.string().email('Invalid email address'), z.literal('demo')]),
5
+ password: z.union([z.string().min(6, 'Password must be at least 6 characters'), z.literal('demo')]),
6
6
  })
7
7
 
8
8
  export const signUpSchema = z
@@ -14,9 +14,29 @@ interface LoginResponse {
14
14
  refreshToken: string
15
15
  }
16
16
 
17
+ /** Build a minimal JWT (not server-verified) for the demo account */
18
+ function makeDemoToken(): string {
19
+ const b64 = (obj: object) =>
20
+ btoa(JSON.stringify(obj)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
21
+ const header = b64({ alg: 'HS256', typ: 'JWT' })
22
+ const payload = b64({ sub: 'demo', roles: ['admin'], permissions: ['*'], exp: 9999999999 })
23
+ return `${header}.${payload}.demo`
24
+ }
25
+
26
+ const DEMO_RESPONSE: LoginResponse = {
27
+ user: { id: 'demo', email: 'demo', name: 'Demo User', roles: ['admin'], permissions: ['*'] },
28
+ accessToken: makeDemoToken(),
29
+ refreshToken: 'demo-refresh',
30
+ }
31
+
17
32
  export const authApi = {
18
- login: (data: SignInValues) =>
19
- apiClient.post<LoginResponse>('/auth/login', data).then((r) => r.data),
33
+ login: (data: SignInValues): Promise<LoginResponse> => {
34
+ // Demo account — works without a real backend
35
+ if (data.email === 'demo' && data.password === 'demo') {
36
+ return Promise.resolve(DEMO_RESPONSE)
37
+ }
38
+ return apiClient.post<LoginResponse>('/auth/login', data).then((r) => r.data)
39
+ },
20
40
 
21
41
  logout: () => apiClient.post('/auth/logout'),
22
42
 
@@ -1,241 +1,305 @@
1
1
  # {{PROJECT_NAME}}
2
2
 
3
- > Vite 7 single-page application (SPA) portal frontend
4
-
5
- Lightning-fast Vite 7 application with TanStack Router, i18n, dark mode, authentication, and Nginx deployment ready.
3
+ > Vite 8 SPA portal — scaffolded by [doo-boilerplate](https://www.npmjs.com/package/doo-boilerplate)
6
4
 
7
5
  ## Quick Start
8
6
 
9
7
  ```bash
10
- pnpm dev
8
+ cp .env.example .env # configure API URLs
9
+ pnpm dev # http://localhost:5173
11
10
  ```
12
11
 
13
- Open [http://localhost:5173](http://localhost:5173) in your browser.
12
+ ---
14
13
 
15
- ## Available Scripts
14
+ ## Scripts
16
15
 
17
16
  | Command | Purpose |
18
17
  |---------|---------|
19
- | `pnpm dev` | Start dev server with HMR |
20
- | `pnpm build` | Build for production |
21
- | `pnpm preview` | Preview production build locally |
22
- | `pnpm lint` | Run ESLint |
23
- | `pnpm lint:fix` | Fix linting issues |
24
- | `pnpm type-check` | TypeScript type checking |
25
- | `pnpm format` | Format code with Prettier |
26
- | `pnpm format:check` | Check formatting without changes |
27
- | `pnpm knip` | Find unused files/exports |
28
- | `pnpm gen:api` | Generate types from Swagger spec |
29
- | `pnpm gen:api:watch` | Watch mode for API generation |
30
- | `pnpm docker:build` | Build & scan Docker image |
31
- | `pnpm docker:scan` | Security scan image with Trivy |
32
-
33
- ## Tech Stack
34
-
35
- - **Bundler:** Vite 7 with SWC (lightning-fast)
36
- - **Routing:** TanStack Router 1 (file-based)
37
- - **Styling:** Tailwind CSS 4 + Shadcn/ui
38
- - **i18n:** i18next with auto-detection
39
- - **State:** Zustand 5 (global) + TanStack Query 5 (server)
40
- - **Forms:** React Hook Form + Zod
41
- - **HTTP:** Axios with JWT interceptors
42
- - **Auth:** JWT store with refresh token persistence
43
- - **DevOps:** Docker (multi-stage) + Nginx + Trivy
18
+ | `pnpm dev` | Dev server with HMR |
19
+ | `pnpm build` | Production build |
20
+ | `pnpm preview` | Preview production build |
21
+ | `pnpm lint` / `lint:fix` | ESLint |
22
+ | `pnpm type-check` | TypeScript |
23
+ | `pnpm format` | Prettier |
24
+ | `pnpm knip` | Find unused code |
25
+ | `pnpm gen:api` | Generate types from `docs/swagger/api.json` |
26
+ | `pnpm gen:api:watch` | Watch mode |
27
+ | `pnpm docker:build` | Build image + Trivy scan |
28
+ | `pnpm docker:scan` | Scan existing image |
44
29
 
45
- ## Environment Variables
30
+ ---
46
31
 
47
- Copy `.env.example` to `.env`:
32
+ ## Folder Structure
48
33
 
49
- ```bash
50
- cp .env.example .env
34
+ ```
35
+ src/
36
+ ├── routes/ # TanStack Router (file-based, type-safe)
37
+ │ ├── __root.tsx # Root layout: providers, loading bar
38
+ │ ├── _authenticated.tsx # Auth guard: redirects to /sign-in if no token
39
+ │ ├── _authenticated/
40
+ │ │ ├── dashboard.tsx # /dashboard
41
+ │ │ ├── profile.tsx # /profile
42
+ │ │ └── settings.tsx # /settings
43
+ │ ├── (auth)/
44
+ │ │ └── sign-in.tsx # /sign-in (public)
45
+ │ └── (errors)/
46
+ │ └── 404.tsx # 404 page
47
+
48
+ ├── routeTree.gen.ts # Auto-generated — NEVER edit manually
49
+
50
+ ├── main.tsx # Entry: i18n init → <RouterProvider>
51
+
52
+ ├── components/
53
+ │ ├── ui/ # Shadcn/ui primitives — DO NOT EDIT
54
+ │ │ └── button, card, dialog, form, input, select, ...
55
+ │ ├── common/ # Shared app-wide components
56
+ │ │ ├── error-boundary.tsx
57
+ │ │ ├── loading-spinner.tsx
58
+ │ │ └── theme-toggle.tsx # Dark/light/system toggle
59
+ │ └── layout/ # Page chrome
60
+ │ ├── sidebar.tsx # Collapsible nav sidebar
61
+ │ ├── header.tsx # Top bar with user menu
62
+ │ └── page-layout.tsx # Wrapper: sidebar + main content
63
+
64
+ ├── context/
65
+ │ └── theme-provider.tsx # next-themes wrapper for Vite
66
+
67
+ ├── features/ # Domain modules (feature-first)
68
+ │ └── auth/
69
+ │ ├── components/
70
+ │ │ └── sign-in-form.tsx
71
+ │ ├── hooks/
72
+ │ │ └── use-auth.ts # useSignIn, useSignOut, useCurrentUser
73
+ │ ├── schemas/
74
+ │ │ └── auth-schema.ts # Zod: LoginSchema
75
+ │ └── services/
76
+ │ ├── auth-api.ts # API calls: login, logout, me
77
+ │ └── gen/ # Auto-generated types (pnpm gen:api)
78
+
79
+ ├── hooks/ # Shared custom hooks
80
+ │ └── use-media-query.ts
81
+
82
+ ├── lib/ # Utilities & singletons
83
+ │ ├── api-client.ts # Axios instance (auto Bearer token + 401 handler)
84
+ │ ├── i18n.ts # i18next init with EN/VI resources
85
+ │ ├── query-client.ts # TanStack Query client config
86
+ │ └── utils.ts # cn() = clsx + tailwind-merge
87
+
88
+ ├── stores/
89
+ │ └── auth-store.ts # Zustand auth (persisted to localStorage)
90
+
91
+ ├── styles/
92
+ │ └── globals.css # @import tailwindcss + CSS variables
93
+
94
+ └── types/
95
+ └── index.ts # Shared TS types
51
96
  ```
52
97
 
53
- In Vite, environment variables must be prefixed with `VITE_`:
98
+ ---
54
99
 
55
- ```env
56
- # API Configuration
57
- VITE_API_URL=http://localhost:8000
58
- VITE_API_AUTH_URL=http://localhost:8001
100
+ ## Design Patterns
59
101
 
60
- # App
61
- VITE_APP_URL=http://localhost:5173
62
- VITE_APP_NAME={{PROJECT_NAME}}
63
- ```
102
+ ### File-Based Routing (TanStack Router)
64
103
 
65
- Access in code:
104
+ Routes are defined by files in `src/routes/`. The router plugin auto-generates `routeTree.gen.ts` on save.
66
105
 
67
- ```typescript
68
- const apiUrl = import.meta.env.VITE_API_URL
69
- ```
106
+ | File naming | Result |
107
+ |-------------|--------|
108
+ | `_authenticated.tsx` | Layout route — all nested routes require auth |
109
+ | `_authenticated/dashboard.tsx` | `/dashboard` (protected) |
110
+ | `(auth)/sign-in.tsx` | `/sign-in` (parentheses = pathless group) |
111
+ | `__root.tsx` | Root layout (wraps everything) |
70
112
 
71
- ## Project Structure
113
+ **Add a new protected page:**
72
114
 
115
+ ```bash
116
+ touch src/routes/_authenticated/reports.tsx
73
117
  ```
74
- src/
75
- ├── routes/
76
- │ ├── __root.tsx # Root route (layout)
77
- │ ├── index.tsx # Home page (/)
78
- │ ├── auth/
79
- │ │ ├── login.tsx # /auth/login
80
- │ │ └── register.tsx # /auth/register
81
- │ ├── dashboard/
82
- │ │ └── index.tsx # /dashboard
83
- │ └── _404.tsx # 404 Not Found
84
- ├── routeTree.gen.ts # Auto-generated by @tanstack/router-plugin
85
- ├── main.tsx # App entry point
86
- ├── components/
87
- │ ├── ui/ # Shadcn/ui components
88
- │ └── **/ # Feature-specific components
89
- ├── features/
90
- │ ├── auth/
91
- │ │ ├── hooks/
92
- │ │ ├── stores/ # Zustand auth store
93
- │ │ ├── services/
94
- │ │ │ └── gen/ # Generated API types (gen:api)
95
- │ │ └── types/
96
- │ └── **/
97
- ├── hooks/ # Custom React hooks
98
- ├── context/ # React context providers
99
- ├── lib/
100
- │ ├── cn.ts # Tailwind merge utility
101
- │ ├── http.ts # Axios instance
102
- │ └── **/
103
- ├── stores/ # Zustand stores
104
- ├── styles/
105
- │ ├── globals.css
106
- │ └── variables.css # CSS variables
107
- └── types/
108
- └── index.ts
118
+
119
+ ```ts
120
+ // src/routes/_authenticated/reports.tsx
121
+ import { createFileRoute } from '@tanstack/react-router'
122
+
123
+ export const Route = createFileRoute('/_authenticated/reports')({
124
+ component: ReportsPage,
125
+ })
126
+
127
+ function ReportsPage() {
128
+ return <PageLayout title="Reports">...</PageLayout>
129
+ }
109
130
  ```
110
131
 
111
- ## TanStack Router Basics
132
+ The route is auto-discovered and added to the type-safe route tree.
112
133
 
113
- ### File-Based Routing
134
+ ### Feature-First Modules
114
135
 
115
- Routes defined by file structure in `src/routes/`:
136
+ Add new domain logic as a self-contained feature:
116
137
 
117
138
  ```
118
- routes/
119
- ├── __root.tsx → Root layout
120
- ├── index.tsx → /
121
- ├── about.tsx → /about
122
- ├── auth/
123
- ├── login.tsx → /auth/login
124
- └── register.tsx → /auth/register
125
- └── dashboard.index.tsx → /dashboard
139
+ features/reports/
140
+ ├── components/report-table.tsx
141
+ ├── hooks/use-reports.ts ← TanStack Query hooks
142
+ ├── schemas/report-schema.ts ← Zod validation
143
+ └── services/
144
+ ├── reports-api.ts ← API calls
145
+ └── gen/ ← Generated types (optional)
126
146
  ```
127
147
 
128
- **Note:** Index files use `index.tsx`, nested routes use folder structure.
148
+ ### State Layers
129
149
 
130
- ### Route Generation
150
+ | State type | Tool | Location |
151
+ |-----------|------|----------|
152
+ | Server / async data | TanStack Query | `features/{x}/hooks/` |
153
+ | Global client state | Zustand | `src/stores/` |
154
+ | Form state | React Hook Form | component-local |
155
+ | URL / search params | TanStack Router | `Route.useSearch()` |
131
156
 
132
- TanStack Router automatically generates `src/routeTree.gen.ts`. **Do not edit** this file manually—it's regenerated on every build.
157
+ **Rule:** Never put fetched data in Zustand. Only truly global client state (auth, UI prefs) goes in stores.
133
158
 
134
- ### Define Route Handlers
159
+ ### Auth Store
135
160
 
136
- ```typescript
137
- // src/routes/auth/login.tsx
138
- import { createFileRoute } from '@tanstack/react-router'
161
+ ```ts
162
+ import { useAuthStore } from '@/stores/auth-store'
139
163
 
140
- export const Route = createFileRoute('/auth/login')({
141
- component: LoginPage,
142
- })
164
+ // Read
165
+ const { user, isAuthenticated } = useAuthStore()
143
166
 
144
- function LoginPage() {
145
- return <div>Login Page</div>
146
- }
167
+ // RBAC
168
+ const isAdmin = useAuthStore(s => s.hasRole('admin'))
169
+ const canPublish = useAuthStore(s => s.hasPermission('publish:articles'))
170
+
171
+ // Mutations
172
+ const { login, logout } = useAuthStore()
147
173
  ```
148
174
 
149
- ### Link Navigation
175
+ Persisted fields: `user`, `accessToken`, `refreshToken`, `isAuthenticated`.
150
176
 
151
- ```typescript
152
- import { Link } from '@tanstack/react-router'
177
+ ### Auth Guard (Route Protection)
153
178
 
154
- export function Nav() {
155
- return (
156
- <nav>
157
- <Link to="/">Home</Link>
158
- <Link to="/auth/login">Login</Link>
159
- </nav>
160
- )
161
- }
179
+ ```ts
180
+ // routes/_authenticated.tsx
181
+ export const Route = createFileRoute('/_authenticated')({
182
+ beforeLoad: () => {
183
+ const { isAuthenticated } = useAuthStore.getState()
184
+ if (!isAuthenticated) throw redirect({ to: '/sign-in' })
185
+ },
186
+ })
162
187
  ```
163
188
 
164
- ## API Code Generation
189
+ All routes nested under `_authenticated/` inherit this guard automatically.
165
190
 
166
- Generate TypeScript types from backend Swagger spec:
191
+ ### API Client
167
192
 
168
- ```bash
169
- # Place spec at docs/swagger/api.json
170
- curl https://api.example.com/swagger.json > docs/swagger/api.json
193
+ ```ts
194
+ import apiClient from '@/lib/api-client'
171
195
 
172
- # Generate types
173
- pnpm gen:api
196
+ // features/reports/services/reports-api.ts
197
+ export const reportsApi = {
198
+ list: (params?: FilterParams) =>
199
+ apiClient.get<Report[]>('/reports', { params }).then(r => r.data),
200
+ create: (data: CreateReportDto) =>
201
+ apiClient.post<Report>('/reports', data).then(r => r.data),
202
+ }
203
+ ```
204
+
205
+ Auto-injects `Authorization: Bearer <token>` and redirects to `/sign-in` on 401.
206
+
207
+ ### TanStack Query Hooks
208
+
209
+ ```ts
210
+ // features/reports/hooks/use-reports.ts
211
+ export function useReports(params?: FilterParams) {
212
+ return useQuery({
213
+ queryKey: ['reports', params],
214
+ queryFn: () => reportsApi.list(params),
215
+ staleTime: 5 * 60_000,
216
+ })
217
+ }
174
218
 
175
- # Watch mode during API development
176
- pnpm gen:api:watch
219
+ export function useCreateReport() {
220
+ const qc = useQueryClient()
221
+ return useMutation({
222
+ mutationFn: reportsApi.create,
223
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['reports'] }),
224
+ })
225
+ }
177
226
  ```
178
227
 
179
- Generated types appear in `src/features/auth/services/gen/`.
228
+ ### Form Pattern
180
229
 
181
- ## Docker Deployment
230
+ ```ts
231
+ const schema = z.object({ title: z.string().min(3) })
232
+ type Values = z.infer<typeof schema>
182
233
 
183
- ### Build & Scan
234
+ function CreateReportForm() {
235
+ const form = useForm<Values>({ resolver: zodResolver(schema) })
236
+ const { mutate, isPending } = useCreateReport()
184
237
 
185
- ```bash
186
- pnpm docker:build
238
+ return (
239
+ <Form {...form}>
240
+ <form onSubmit={form.handleSubmit(data => mutate(data))}>
241
+ <FormField control={form.control} name="title" render={...} />
242
+ <Button disabled={isPending}>Create</Button>
243
+ </form>
244
+ </Form>
245
+ )
246
+ }
187
247
  ```
188
248
 
189
- Builds optimized production bundle and scans for vulnerabilities with Trivy.
249
+ ### Internationalization
190
250
 
191
- ### Local Development
251
+ ```ts
252
+ import { useTranslation } from 'react-i18next'
192
253
 
193
- ```bash
194
- docker-compose up
195
- # App runs on http://localhost:3000
254
+ function Dashboard() {
255
+ const { t, i18n } = useTranslation('dashboard')
256
+ return <h1>{t('welcome')}</h1>
257
+ }
258
+
259
+ // Switch language
260
+ i18n.changeLanguage('vi')
196
261
  ```
197
262
 
198
- Environment variables read from `.env` file.
263
+ Translation resources are in `src/lib/i18n.ts`. Add new namespaces or languages there.
199
264
 
200
- ### Production Build
265
+ ---
201
266
 
202
- The Docker image includes Nginx for SPA routing. All requests fallback to `index.html`:
267
+ ## API Type Generation
203
268
 
204
269
  ```bash
205
- docker build -t my-app:latest \
206
- --build-arg VITE_API_URL=https://api.example.com .
270
+ # 1. Place your backend OpenAPI spec
271
+ curl https://api.example.com/swagger.json > docs/swagger/api.json
207
272
 
208
- docker run -p 80:80 my-app:latest
209
- # Access at http://localhost
273
+ # 2. Generate TypeScript types
274
+ pnpm gen:api
275
+
276
+ # 3. Import generated types
277
+ import type { LoginRequest } from '@/features/auth/services/gen'
210
278
  ```
211
279
 
212
- ### Nginx Configuration
280
+ ---
213
281
 
214
- Nginx config at `nginx.conf` handles SPA routing (all requests → `index.html`). No changes needed for typical deployments.
282
+ ## Environment Variables
283
+
284
+ ```env
285
+ VITE_API_URL=http://localhost:8000
286
+ VITE_API_AUTH_URL=http://localhost:8001
287
+ VITE_APP_NAME={{PROJECT_NAME}}
288
+ ```
215
289
 
216
- ## Vite Configuration
290
+ Access in code: `import.meta.env.VITE_API_URL`
217
291
 
218
- Key Vite settings in `vite.config.ts`:
292
+ ---
219
293
 
220
- ```typescript
221
- import { defineConfig } from 'vite'
222
- import react from '@vitejs/plugin-react-swc'
223
- import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
294
+ ## Docker
224
295
 
225
- export default defineConfig({
226
- plugins: [
227
- TanStackRouterVite(), // Auto-generate routes
228
- react(),
229
- ],
230
- server: {
231
- port: 5173,
232
- },
233
- build: {
234
- target: 'ES2020',
235
- },
236
- })
296
+ ```bash
297
+ pnpm docker:build # Build + Trivy scan
298
+ docker-compose up # Local dev stack on :3000
237
299
  ```
238
300
 
301
+ Vite builds a static bundle served by Nginx. The `nginx.conf` uses `try_files $uri /index.html` for SPA routing.
302
+
239
303
  ---
240
304
 
241
- **Learn more:** [Vite Docs](https://vitejs.dev) | [TanStack Router Docs](https://tanstack.com/router)
305
+ **Framework:** Vite 8 + React 19 · **Routing:** TanStack Router (file-based) · **Styling:** Tailwind CSS 4 + Shadcn/ui · **i18n:** i18next (EN/VI) · **State:** Zustand 5 + TanStack Query 5 · **Node:** ≥20.19.0
@@ -25,7 +25,7 @@
25
25
  "react-i18next": "^15.5.2",
26
26
  "i18next-browser-languagedetector": "^8.0.5",
27
27
  "next-themes": "^0.4.6",
28
- "@tanstack/react-router": "^1.132.47",
28
+ "@tanstack/react-router": "^1.166.12",
29
29
  "@tanstack/react-query": "^5.90.2",
30
30
  "@tanstack/react-table": "^8.21.3",
31
31
  "zustand": "^5.0.8",
@@ -63,13 +63,13 @@
63
63
  "@types/react-dom": "^19",
64
64
  "@types/node": "^22",
65
65
  "typescript": "^5.9.3",
66
- "@vitejs/plugin-react-swc": "^4.1.0",
67
- "vite": "^7.1.9",
68
- "@tailwindcss/vite": "^4.1.14",
69
- "tailwindcss": "^4.1.14",
70
- "@tanstack/router-plugin": "^1.132.47",
66
+ "@vitejs/plugin-react-swc": "^4.3.0",
67
+ "vite": "^8.0.0",
68
+ "@tailwindcss/vite": "^4.2.1",
69
+ "tailwindcss": "^4.2.1",
70
+ "@tanstack/router-plugin": "^1.166.12",
71
71
  "@tanstack/react-query-devtools": "^5.90.2",
72
- "@tanstack/router-devtools": "^1.132.47",
72
+ "@tanstack/router-devtools": "^1.166.12",
73
73
  "eslint": "^9",
74
74
  "eslint-plugin-react-hooks": "^5.2.0",
75
75
  "eslint-plugin-unused-imports": "^4.1.4",
@@ -87,5 +87,8 @@
87
87
  },
88
88
  "lint-staged": {
89
89
  "*.{js,ts,tsx,css}": ["eslint --fix", "prettier --write"]
90
+ },
91
+ "engines": {
92
+ "node": ">=20.19.0"
90
93
  }
91
94
  }
@@ -25,7 +25,7 @@ export function SignInForm() {
25
25
  <div className='text-center'>
26
26
  <h1 className='text-2xl font-bold tracking-tight'>Sign in</h1>
27
27
  <p className='mt-1 text-sm text-muted-foreground'>
28
- Enter your credentials to access your account
28
+ Enter your credentials to access your account · or try <strong>demo / demo</strong>
29
29
  </p>
30
30
  </div>
31
31
 
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  export const signInSchema = z.object({
4
- email: z.string().email('Please enter a valid email address'),
4
+ email: z.union([z.string().email('Please enter a valid email address'), z.literal('demo')]),
5
5
  password: z.string().min(1, 'Password is required'),
6
6
  })
7
7
 
@@ -14,9 +14,29 @@ interface LoginResponse {
14
14
  refreshToken: string
15
15
  }
16
16
 
17
+ /** Build a minimal JWT (not server-verified) for the demo account */
18
+ function makeDemoToken(): string {
19
+ const b64 = (obj: object) =>
20
+ btoa(JSON.stringify(obj)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
21
+ const header = b64({ alg: 'HS256', typ: 'JWT' })
22
+ const payload = b64({ sub: 'demo', roles: ['admin'], permissions: ['*'], exp: 9999999999 })
23
+ return `${header}.${payload}.demo`
24
+ }
25
+
26
+ const DEMO_RESPONSE: LoginResponse = {
27
+ user: { id: 'demo', email: 'demo', name: 'Demo User', roles: ['admin'], permissions: ['*'] },
28
+ accessToken: makeDemoToken(),
29
+ refreshToken: 'demo-refresh',
30
+ }
31
+
17
32
  export const authApi = {
18
- login: (data: SignInValues) =>
19
- apiClient.post<LoginResponse>('/auth/login', data).then((r) => r.data),
33
+ login: (data: SignInValues): Promise<LoginResponse> => {
34
+ // Demo account — works without a real backend
35
+ if (data.email === 'demo' && data.password === 'demo') {
36
+ return Promise.resolve(DEMO_RESPONSE)
37
+ }
38
+ return apiClient.post<LoginResponse>('/auth/login', data).then((r) => r.data)
39
+ },
20
40
 
21
41
  logout: () => apiClient.post('/auth/logout'),
22
42
 
@@ -15,7 +15,7 @@ export default defineConfig({
15
15
  },
16
16
  build: {
17
17
  target: 'esnext',
18
- rollupOptions: {
18
+ rolldownOptions: {
19
19
  output: {
20
20
  manualChunks: {
21
21
  vendor: ['react', 'react-dom'],