doo-boilerplate 0.2.14 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doo-boilerplate",
3
- "version": "0.2.14",
3
+ "version": "0.3.0",
4
4
  "description": "CLI to scaffold Pila portal frontend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,10 @@
8
8
  "create-pila-app": "./dist/index.js"
9
9
  },
10
10
  "main": "./dist/index.js",
11
- "files": ["dist", "templates"],
11
+ "files": [
12
+ "dist",
13
+ "templates"
14
+ ],
12
15
  "scripts": {
13
16
  "build": "tsup",
14
17
  "dev": "tsup --watch",
@@ -0,0 +1,24 @@
1
+ # Debug an Issue
2
+
3
+ Use this prompt to systematically debug a problem in this project.
4
+
5
+ ## Prompt
6
+
7
+ ```
8
+ Debug this issue: [DESCRIBE THE BUG]
9
+
10
+ Steps:
11
+ 1. Reproduce — identify exact steps to trigger the issue
12
+ 2. Locate — search relevant files:
13
+ - Error in UI? → src/features/ or src/components/
14
+ - API error? → src/lib/api-client.ts, src/features/*/data/
15
+ - Route issue? → src/routes/, routeTree.gen.ts
16
+ - State issue? → src/stores/
17
+ 3. Read error — check browser console / Sentry for stack trace
18
+ 4. Root cause — trace the call chain, identify the failing assertion
19
+ 5. Fix — minimal change, do not refactor unrelated code
20
+ 6. Verify:
21
+ - pnpm lint && pnpm type-check
22
+ - Manually test the fixed flow
23
+ 7. Commit: fix([scope]): [what was broken and how fixed]
24
+ ```
@@ -0,0 +1,31 @@
1
+ # Implement a Feature
2
+
3
+ Use this prompt to implement a new feature module in this project.
4
+
5
+ ## Prompt
6
+
7
+ ```
8
+ Implement the feature: [FEATURE_NAME]
9
+
10
+ Context:
11
+ - Read CLAUDE.md for project conventions
12
+ - Feature dir: src/features/[name]/
13
+ - Route: src/routes/_authenticated/[name].tsx
14
+ - Nav: src/components/layout/sidebar.tsx
15
+
16
+ Steps:
17
+ 1. Create src/features/[name]/ with:
18
+ - types/[name].types.ts — TypeScript interfaces
19
+ - schemas/[name].schema.ts — zod validation schemas
20
+ - data/use-[name].ts — TanStack Query hooks
21
+ - components/[name]-list.tsx — list view using DataTable
22
+ - components/[name]-form.tsx — create/edit form
23
+ 2. Add route file at src/routes/_authenticated/[name].tsx
24
+ 3. Register nav item in src/components/layout/sidebar.tsx
25
+ 4. Wire API calls through src/lib/api-client.ts
26
+
27
+ After implementation:
28
+ - Run: pnpm lint && pnpm type-check
29
+ - Fix any lint/type errors before finishing
30
+ - Follow conventional commits: feat([name]): add [description]
31
+ ```
@@ -0,0 +1,30 @@
1
+ # Plan an Implementation
2
+
3
+ Use this prompt to plan a feature or change before writing code.
4
+
5
+ ## Prompt
6
+
7
+ ```
8
+ Plan the implementation for: [FEATURE OR CHANGE]
9
+
10
+ Steps:
11
+ 1. Read CLAUDE.md for project context and conventions
12
+ 2. Analyze requirements — list what needs to be built
13
+ 3. Identify affected files:
14
+ - New feature? → src/features/[name]/
15
+ - New route? → src/routes/_authenticated/
16
+ - Shared component? → src/components/
17
+ - API change? → src/lib/api-client.ts
18
+ 4. Define phases (if complex):
19
+ - Phase 1: types + schemas
20
+ - Phase 2: API hooks (TanStack Query)
21
+ - Phase 3: UI components
22
+ - Phase 4: route + nav integration
23
+ 5. Success criteria:
24
+ - pnpm lint && pnpm type-check pass
25
+ - Feature works end-to-end
26
+ - No regressions in existing routes
27
+ 6. Risks: list potential blockers or edge cases
28
+
29
+ Output: numbered implementation steps ready to execute
30
+ ```
@@ -0,0 +1,24 @@
1
+ # Code Review
2
+
3
+ Use this prompt to review code changes in this project.
4
+
5
+ ## Prompt
6
+
7
+ ```
8
+ Review the following code changes: [FILE(S) OR DIFF]
9
+
10
+ Checklist:
11
+ - [ ] TypeScript types are explicit (no `any` unless justified)
12
+ - [ ] Error handling: API calls wrapped in try/catch or TanStack Query error state
13
+ - [ ] Naming: kebab-case files, camelCase vars, PascalCase components
14
+ - [ ] No unused imports/exports (run `pnpm knip` to verify)
15
+ - [ ] Zod schemas match API response shape
16
+ - [ ] No hardcoded strings — use i18n keys or constants
17
+ - [ ] No secrets or API keys in code
18
+ - [ ] lint-staged passes: pnpm lint && pnpm format:check
19
+ - [ ] Type check passes: pnpm type-check
20
+ - [ ] Follows feature-based structure in src/features/
21
+
22
+ Rate each: Pass / Warn / Fail
23
+ Summary: overall assessment + top 3 issues to fix
24
+ ```
@@ -0,0 +1 @@
1
+ pnpm commitlint --edit $1
@@ -0,0 +1 @@
1
+ pnpm lint-staged
@@ -1,47 +1,135 @@
1
- # CLAUDE.md
1
+ # {{PROJECT_NAME}}
2
2
 
3
3
  This file provides guidance to Claude Code when working in this project.
4
4
 
5
5
  ## Tech Stack
6
- - React 19, TypeScript, Vite 8
7
- - TanStack Router (file-based routing) add routes in `src/routes/`
8
- - TanStack Query data fetching via `useQuery`/`useMutation`
9
- - TanStack Table data tables via `src/components/data-table/`
10
- - Tailwind CSS 4 + shadcn/ui components in `src/components/ui/`
11
- - Zustand global state in `src/stores/`
12
- - react-hook-form + zod — forms with validation
13
- - Sentry — error tracking (set `VITE_SENTRY_DSN` in `.env`)
6
+
7
+ - Vite 8, React 19, TypeScript 5
8
+ - Tailwind CSS 4 + shadcn/ui (Radix UI primitives)
9
+ - TanStack Router (file-based routing), TanStack Query, TanStack Table
10
+ - Zustand (global state), react-hook-form + zod (forms)
11
+ - i18next (internationalization), Sentry (error tracking), Axios (HTTP)
14
12
 
15
13
  ## Project Structure
16
- - `src/routes/` — file-based routes (TanStack Router auto-generates `routeTree.gen.ts`)
17
- - `src/features/` — feature modules (each has components/, hooks/, types/, schemas/, data/)
18
- - `src/components/ui/` — shadcn/ui components
19
- - `src/components/data-table/`reusable DataTable components
20
- - `src/components/layout/` Sidebar, Header, PageLayout
21
- - `src/lib/` — utilities (api-client, query-client, sentry, utils)
22
- - `src/stores/` — Zustand stores
23
-
24
- ## Development Commands
14
+
15
+ ```
16
+ src/
17
+ routes/ # file-based routes auto-generates routeTree.gen.ts
18
+ features/ # feature modules: components/, hooks/, services/, types/, schemas/
19
+ components/
20
+ ui/ # shadcn/ui components
21
+ data-table/ # reusable DataTable
22
+ layout/ # Sidebar, Header, PageLayout
23
+ lib/ # api-client, query-client, sentry, utils
24
+ stores/ # Zustand stores
25
+ config/ # app configuration
26
+ context/ # React contexts
27
+ hooks/ # shared hooks
28
+ types/ # shared types
29
+ ```
30
+
31
+ ## Dev Commands
32
+
25
33
  ```bash
26
- pnpm dev # start dev server
27
- pnpm build # build for production
28
- pnpm type-check # TypeScript check
29
- pnpm lint # ESLint
34
+ pnpm dev # start dev server
35
+ pnpm build # TypeScript check + Vite build
36
+ pnpm lint # ESLint check
37
+ pnpm lint:fix # ESLint auto-fix
38
+ pnpm type-check # tsc --noEmit
39
+ pnpm format # Prettier write
40
+ pnpm format:check # Prettier check
41
+ pnpm knip # detect unused exports/deps
42
+ pnpm docker:build # build Docker image + Trivy scan
43
+ pnpm docker:scan # Trivy security scan only
30
44
  ```
31
45
 
32
- ## Adding a New Feature Module
33
- 1. Create `src/features/{name}/` with: types/, schemas/, components/, data/
46
+ ## Code Conventions
47
+
48
+ - **File naming:** kebab-case
49
+ - **Feature structure:** group by domain in `src/features/{name}/`
50
+ - **Commits:** conventional commits enforced by commitlint + Husky
51
+ - **Pre-commit:** ESLint + Prettier run automatically via lint-staged
52
+ - **Routes:** auto-discovered — create file in `src/routes/`, run `pnpm dev` to regenerate
53
+
54
+ ## Adding a Feature
55
+
56
+ 1. Create `src/features/{name}/` with: `types/`, `schemas/`, `services/`, `hooks/`, `components/`
34
57
  2. Add route at `src/routes/_authenticated/{name}.tsx`
35
58
  3. Add nav item in `src/components/layout/sidebar.tsx`
36
- 4. Use `DataTable` from `src/components/data-table/data-table.tsx` for list views
59
+ 4. Use `DataTable` from `src/components/data-table/` for list views
60
+ 5. Use TanStack Query hooks + `src/lib/api-client.ts` for API calls
37
61
 
38
- ## Adding New Routes
39
- Routes are auto-discovered. Create a file in `src/routes/` and run `pnpm dev` to regenerate `routeTree.gen.ts`.
62
+ ## Environment Variables
40
63
 
41
- ## API Integration
42
- Replace mock data in `src/features/*/data/` with real API calls using `src/lib/api-client.ts` (axios).
43
- Use TanStack Query hooks for data fetching.
64
+ Copy `.env.example` to `.env`. Key vars:
65
+ - `VITE_SENTRY_DSN` Sentry DSN (optional)
44
66
 
45
- ## Environment Variables
46
- Copy `.env.example` to `.env` and fill in values.
47
- `VITE_SENTRY_DSN` Sentry DSN for error tracking (optional)
67
+ ## Library-Specific Rules
68
+
69
+ ### Tailwind CSS v4
70
+ - No `tailwind.config.js` — config lives in CSS via `@theme` block
71
+ - Use `@utility` to define custom utilities, NOT `@apply` for component styles
72
+ - CSS variables for design tokens: `var(--color-primary)`, `var(--spacing-4)`
73
+ - Import: `import '@/styles/globals.css'` (single entry, no per-component imports)
74
+ - Dark mode: use `dark:` variant, toggled via `next-themes` `ThemeProvider`
75
+
76
+ ### TanStack Router
77
+ - File-based routing only — create files in `src/routes/`, never manually edit `routeTree.gen.ts`
78
+ - Every route file must export via `createFileRoute('...')` — path must match filename exactly
79
+ - Authenticated routes go under `src/routes/_authenticated/` (wrapped by auth layout)
80
+ - Navigation: `useNavigate()` hook or `<Link to="...">`, never `window.location`
81
+ - Params: `const { id } = Route.useParams()`, search: `Route.useSearch()`
82
+ - Lazy load heavy routes: `export const Route = createLazyFileRoute(...)`
83
+
84
+ ### TanStack Query
85
+ - Prefer `useSuspenseQuery` over `useQuery` — wrap route component in `<Suspense>`
86
+ - Query keys: array format `['resource', id]` or `['resource', 'list', filters]`
87
+ - Mutations: `useMutation` + `onSuccess: () => queryClient.invalidateQueries(...)`
88
+ - Global config in `src/lib/query-client.ts` — do not create new QueryClient instances
89
+ - All API calls go through `src/lib/api-client.ts` (axios instance with auth headers)
90
+
91
+ ### shadcn/ui
92
+ - Components live in `src/components/ui/` — do not edit them directly
93
+ - Add new components: `pnpm dlx shadcn@latest add <component>`
94
+ - Compose with Radix primitives when shadcn doesn't have what you need
95
+ - Use `cn()` from `src/lib/utils.ts` for conditional class merging (not `clsx` directly)
96
+
97
+ ### react-hook-form + zod
98
+ - Always define schema with `z.object({...})` in `src/features/{name}/schemas/`
99
+ - Use `useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema) })`
100
+ - Never use uncontrolled inputs — always register with `{...register('field')}`
101
+
102
+ ### Zustand
103
+ - One store per feature domain in `src/stores/`
104
+ - Never store server data in Zustand — server state belongs in TanStack Query
105
+ - Use immer middleware for nested state updates
106
+
107
+ ### i18n
108
+ - All user-facing strings must use `useTranslation()` — no hardcoded UI text
109
+ - Key format: `"feature.action.noun"` (e.g. `"user.form.submit"`)
110
+ - Translation files: `src/locales/{en,vi}/translation.json`
111
+
112
+ ## API Patterns
113
+
114
+ - Auth: Bearer token in `Authorization` header — handled automatically by `src/lib/api-client.ts` interceptor
115
+ - Success response shape: `{ data: T, message: string, success: boolean }`
116
+ - Error response shape: `{ message: string, errors: Record<string, string[]> }`
117
+ - Base URL: `import.meta.env.VITE_API_URL`
118
+ - Never create axios instances directly — always import from `src/lib/api-client.ts`
119
+
120
+ ## Anti-Patterns — Never Do These
121
+
122
+ - **No `useEffect` for data fetching** — use TanStack Query (`useSuspenseQuery` / `useQuery`)
123
+ - **No `React.FC`** — use `function Component(): JSX.Element`
124
+ - **No `any`** — use `unknown` + type guard or proper types
125
+ - **No business logic in route files** — put in `src/features/`
126
+ - **No direct edits to `src/components/ui/`** — add components via `pnpm dlx shadcn@latest add`
127
+ - **No manual edits to `routeTree.gen.ts`** — auto-generated, regenerates on `pnpm dev`
128
+ - **No new `QueryClient` instances** — use the one from `src/lib/query-client.ts`
129
+ - **No hardcoded UI strings** — use i18n keys
130
+
131
+ ## Claude AI Workflow
132
+
133
+ - Slash commands available: `/feature`, `/debug`, `/review`, `/plan` (see `.claude/commands/`)
134
+ - Always run `pnpm lint && pnpm type-check` after code changes
135
+ - Follow conventional commits: `feat:`, `fix:`, `chore:`, `refactor:`, `docs:`
@@ -1,6 +1,5 @@
1
- # API Configuration
1
+ # API
2
2
  VITE_API_URL=http://localhost:8000
3
- VITE_API_AUTH_URL=http://localhost:8001
4
3
 
5
4
  # App
6
5
  VITE_APP_URL=http://localhost:3000
@@ -16,7 +16,8 @@
16
16
  "gen:api": "swagger-typescript-api -p ./docs/swagger/api.json -o ./src/features/auth/services/gen --no-client --modular",
17
17
  "gen:api:watch": "swagger-typescript-api -p ./docs/swagger/api.json -o ./src/features/auth/services/gen --no-client --modular --watch",
18
18
  "docker:build": "bash scripts/build-and-scan.sh",
19
- "docker:scan": "bash scripts/trivy-scan.sh"
19
+ "docker:scan": "bash scripts/trivy-scan.sh",
20
+ "prepare": "is-ci || husky"
20
21
  },
21
22
  "dependencies": {
22
23
  "@sentry/react": "^9.0.0",
@@ -87,7 +88,10 @@
87
88
  "prettier": "^3.6.2",
88
89
  "prettier-plugin-tailwindcss": "^0.6.11",
89
90
  "@trivago/prettier-plugin-sort-imports": "^4.3.0",
91
+ "@commitlint/cli": "^19.8.1",
92
+ "@commitlint/config-conventional": "^19.8.1",
90
93
  "husky": "^9.1.7",
94
+ "is-ci": "^3.0.1",
91
95
  "lint-staged": "^15.2.11",
92
96
  "knip": "^5.64.2",
93
97
  "swagger-typescript-api": "^13.2.7",
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ['@commitlint/config-conventional'],
3
+ }
@@ -11,11 +11,10 @@ import {
11
11
  type VisibilityState,
12
12
  } from '@tanstack/react-table'
13
13
  import { Plus } from 'lucide-react'
14
- import { toast } from 'sonner'
15
14
  import { Button } from '@/components/ui/button'
16
15
  import { DataTable } from '@/components/data-table/data-table'
17
16
  import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
18
- import { tasksMockData } from '../data/tasks-mock-data'
17
+ import { useTasks, useCreateTask, useUpdateTask, useDeleteTask } from '../hooks/use-tasks'
19
18
  import { getTaskColumns } from './tasks-table-columns'
20
19
  import { TaskFormDialog } from './task-form-dialog'
21
20
  import type { Task } from '../types/task'
@@ -34,9 +33,13 @@ const PRIORITY_FILTER_OPTIONS = [
34
33
  { label: 'High', value: 'high' },
35
34
  ]
36
35
 
37
- /** Full CRUD data table for tasks using local state */
36
+ /** CRUD data table for tasks data fetched via TanStack Query, mutations invalidate cache */
38
37
  export function TasksCrudTable() {
39
- const [tasks, setTasks] = useState<Task[]>(tasksMockData)
38
+ const { data: response, isLoading } = useTasks()
39
+ const createTask = useCreateTask()
40
+ const updateTask = useUpdateTask()
41
+ const deleteTask = useDeleteTask()
42
+
40
43
  const [sorting, setSorting] = useState<SortingState>([])
41
44
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
42
45
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
@@ -45,36 +48,23 @@ export function TasksCrudTable() {
45
48
  const [dialogOpen, setDialogOpen] = useState(false)
46
49
  const [editingTask, setEditingTask] = useState<Task | undefined>()
47
50
 
48
- const handleEdit = (task: Task) => {
49
- setEditingTask(task)
50
- setDialogOpen(true)
51
- }
52
-
53
- const handleDelete = (id: string) => {
54
- setTasks((prev) => prev.filter((t) => t.id !== id))
55
- toast.success('Task deleted')
56
- }
51
+ const handleEdit = (task: Task) => { setEditingTask(task); setDialogOpen(true) }
52
+ const handleDelete = (id: string) => deleteTask.mutate(id)
57
53
 
58
54
  const handleSubmit = (values: TaskFormValues) => {
59
55
  if (editingTask) {
60
- setTasks((prev) => prev.map((t) => t.id === editingTask.id ? { ...t, ...values } : t))
61
- toast.success('Task updated')
56
+ updateTask.mutate({ id: editingTask.id, data: values })
62
57
  } else {
63
- const newTask: Task = {
64
- id: String(Date.now()),
65
- ...values,
66
- createdAt: new Date().toISOString().slice(0, 10),
67
- }
68
- setTasks((prev) => [newTask, ...prev])
69
- toast.success('Task created')
58
+ createTask.mutate(values)
70
59
  }
71
60
  setEditingTask(undefined)
72
61
  }
73
62
 
63
+ const data = response?.data ?? []
74
64
  const columns = getTaskColumns({ onEdit: handleEdit, onDelete: handleDelete })
75
65
 
76
66
  const table = useReactTable({
77
- data: tasks,
67
+ data,
78
68
  columns,
79
69
  state: { sorting, columnFilters, columnVisibility, columnOrder, rowSelection },
80
70
  onSortingChange: setSorting,
@@ -103,6 +93,7 @@ export function TasksCrudTable() {
103
93
  <Button
104
94
  size='sm'
105
95
  className='ml-2'
96
+ disabled={isLoading}
106
97
  onClick={() => { setEditingTask(undefined); setDialogOpen(true) }}
107
98
  >
108
99
  <Plus className='mr-1 h-4 w-4' /> Add Task
@@ -0,0 +1,61 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { toast } from 'sonner'
3
+ import { tasksApi, type CreateTaskPayload, type UpdateTaskPayload, type TaskListParams } from '../services/tasks-api'
4
+
5
+ export const TASK_QUERY_KEY = 'tasks'
6
+
7
+ /** Fetch paginated task list */
8
+ export function useTasks(params?: TaskListParams) {
9
+ return useQuery({
10
+ queryKey: [TASK_QUERY_KEY, params],
11
+ queryFn: () => tasksApi.list(params),
12
+ })
13
+ }
14
+
15
+ /** Fetch single task by id */
16
+ export function useTask(id: string) {
17
+ return useQuery({
18
+ queryKey: [TASK_QUERY_KEY, id],
19
+ queryFn: () => tasksApi.getById(id),
20
+ enabled: !!id,
21
+ })
22
+ }
23
+
24
+ /** Create a new task */
25
+ export function useCreateTask() {
26
+ const queryClient = useQueryClient()
27
+ return useMutation({
28
+ mutationFn: (data: CreateTaskPayload) => tasksApi.create(data),
29
+ onSuccess: () => {
30
+ queryClient.invalidateQueries({ queryKey: [TASK_QUERY_KEY] })
31
+ toast.success('Task created')
32
+ },
33
+ onError: () => toast.error('Failed to create task'),
34
+ })
35
+ }
36
+
37
+ /** Update an existing task */
38
+ export function useUpdateTask() {
39
+ const queryClient = useQueryClient()
40
+ return useMutation({
41
+ mutationFn: ({ id, data }: { id: string; data: UpdateTaskPayload }) => tasksApi.update(id, data),
42
+ onSuccess: () => {
43
+ queryClient.invalidateQueries({ queryKey: [TASK_QUERY_KEY] })
44
+ toast.success('Task updated')
45
+ },
46
+ onError: () => toast.error('Failed to update task'),
47
+ })
48
+ }
49
+
50
+ /** Delete a task by id */
51
+ export function useDeleteTask() {
52
+ const queryClient = useQueryClient()
53
+ return useMutation({
54
+ mutationFn: (id: string) => tasksApi.delete(id),
55
+ onSuccess: () => {
56
+ queryClient.invalidateQueries({ queryKey: [TASK_QUERY_KEY] })
57
+ toast.success('Task deleted')
58
+ },
59
+ onError: () => toast.error('Failed to delete task'),
60
+ })
61
+ }
@@ -0,0 +1,32 @@
1
+ import apiClient from '@/lib/api-client'
2
+ import type { PaginatedResponse } from '@/types'
3
+ import type { Task } from '../types/task'
4
+
5
+ export interface TaskListParams {
6
+ page?: number
7
+ limit?: number
8
+ search?: string
9
+ status?: string
10
+ priority?: string
11
+ }
12
+
13
+ export type CreateTaskPayload = Omit<Task, 'id' | 'createdAt'>
14
+ export type UpdateTaskPayload = Partial<CreateTaskPayload>
15
+
16
+ /** Raw API calls for the tasks resource — always use hooks (use-tasks.ts) in components */
17
+ export const tasksApi = {
18
+ list: (params?: TaskListParams): Promise<PaginatedResponse<Task>> =>
19
+ apiClient.get('/tasks', { params }).then((r) => r.data),
20
+
21
+ getById: (id: string): Promise<Task> =>
22
+ apiClient.get(`/tasks/${id}`).then((r) => r.data),
23
+
24
+ create: (data: CreateTaskPayload): Promise<Task> =>
25
+ apiClient.post('/tasks', data).then((r) => r.data),
26
+
27
+ update: (id: string, data: UpdateTaskPayload): Promise<Task> =>
28
+ apiClient.put(`/tasks/${id}`, data).then((r) => r.data),
29
+
30
+ delete: (id: string): Promise<void> =>
31
+ apiClient.delete(`/tasks/${id}`).then((r) => r.data),
32
+ }
@@ -17,13 +17,13 @@ import { PlusCircle } from 'lucide-react'
17
17
  import { Button } from '@/components/ui/button'
18
18
  import { DataTable } from '@/components/data-table/data-table'
19
19
  import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
20
- import { mockUsers } from '../data/users-mock-data'
20
+ import { useUsers, useCreateUser, useUpdateUser, useDeleteUser } from '../hooks/use-users'
21
21
  import { getUsersTableColumns } from './users-table-columns'
22
22
  import { UserFormDialog } from './user-form-dialog'
23
23
  import { UserDeleteConfirmationDialog } from './user-delete-confirmation-dialog'
24
24
  import type { User } from '../types/user'
25
+ import type { CreateUserPayload, UpdateUserPayload } from '../services/users-api'
25
26
 
26
- /** Filter options for role and status faceted filters */
27
27
  const roleFilterOptions = [
28
28
  { label: 'Admin', value: 'admin' },
29
29
  { label: 'Manager', value: 'manager' },
@@ -42,57 +42,40 @@ const toolbarFilters = [
42
42
  ]
43
43
 
44
44
  export function UsersTable() {
45
- const [data, setData] = useState<User[]>(mockUsers)
45
+ const { data: response, isLoading } = useUsers()
46
+ const createUser = useCreateUser()
47
+ const updateUser = useUpdateUser()
48
+ const deleteUser = useDeleteUser()
49
+
46
50
  const [sorting, setSorting] = useState<SortingState>([])
47
51
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
48
52
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
49
53
  const [columnOrder, setColumnOrder] = useState<ColumnOrderState>([])
50
54
  const [rowSelection, setRowSelection] = useState({})
51
-
52
- // Dialog state
53
55
  const [formDialogOpen, setFormDialogOpen] = useState(false)
54
56
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
55
57
  const [selectedUser, setSelectedUser] = useState<User | null>(null)
56
58
 
57
- const handleEdit = (user: User) => {
58
- setSelectedUser(user)
59
- setFormDialogOpen(true)
60
- }
61
-
62
- const handleDelete = (user: User) => {
63
- setSelectedUser(user)
64
- setDeleteDialogOpen(true)
65
- }
66
-
67
- const handleAddNew = () => {
68
- setSelectedUser(null)
69
- setFormDialogOpen(true)
70
- }
59
+ const handleEdit = (user: User) => { setSelectedUser(user); setFormDialogOpen(true) }
60
+ const handleDelete = (user: User) => { setSelectedUser(user); setDeleteDialogOpen(true) }
61
+ const handleAddNew = () => { setSelectedUser(null); setFormDialogOpen(true) }
71
62
 
72
63
  const handleFormSubmit = (values: Omit<User, 'id' | 'createdAt'>) => {
73
64
  if (selectedUser) {
74
- // Edit existing user
75
- setData((prev) => prev.map((u) => (u.id === selectedUser.id ? { ...u, ...values } : u)))
65
+ updateUser.mutate({ id: selectedUser.id, data: values as UpdateUserPayload })
76
66
  } else {
77
- // Add new user
78
- const newUser: User = {
79
- ...values,
80
- id: String(Date.now()),
81
- createdAt: new Date().toISOString().split('T')[0],
82
- }
83
- setData((prev) => [newUser, ...prev])
67
+ createUser.mutate(values as CreateUserPayload)
84
68
  }
85
69
  setFormDialogOpen(false)
86
70
  }
87
71
 
88
72
  const handleConfirmDelete = () => {
89
- if (selectedUser) {
90
- setData((prev) => prev.filter((u) => u.id !== selectedUser.id))
91
- }
73
+ if (selectedUser) deleteUser.mutate(selectedUser.id)
92
74
  setDeleteDialogOpen(false)
93
75
  setSelectedUser(null)
94
76
  }
95
77
 
78
+ const data = response?.data ?? []
96
79
  const columns = getUsersTableColumns({ onEdit: handleEdit, onDelete: handleDelete })
97
80
 
98
81
  const table = useReactTable({
@@ -121,7 +104,7 @@ export function UsersTable() {
121
104
  searchPlaceholder='Search users...'
122
105
  filters={toolbarFilters}
123
106
  />
124
- <Button size='sm' className='ml-4' onClick={handleAddNew}>
107
+ <Button size='sm' className='ml-4' onClick={handleAddNew} disabled={isLoading}>
125
108
  <PlusCircle className='mr-2 h-4 w-4' />
126
109
  Add User
127
110
  </Button>
@@ -0,0 +1,61 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import { toast } from 'sonner'
3
+ import { usersApi, type CreateUserPayload, type UpdateUserPayload, type UserListParams } from '../services/users-api'
4
+
5
+ export const USER_QUERY_KEY = 'users'
6
+
7
+ /** Fetch paginated user list */
8
+ export function useUsers(params?: UserListParams) {
9
+ return useQuery({
10
+ queryKey: [USER_QUERY_KEY, params],
11
+ queryFn: () => usersApi.list(params),
12
+ })
13
+ }
14
+
15
+ /** Fetch single user by id */
16
+ export function useUser(id: string) {
17
+ return useQuery({
18
+ queryKey: [USER_QUERY_KEY, id],
19
+ queryFn: () => usersApi.getById(id),
20
+ enabled: !!id,
21
+ })
22
+ }
23
+
24
+ /** Create a new user */
25
+ export function useCreateUser() {
26
+ const queryClient = useQueryClient()
27
+ return useMutation({
28
+ mutationFn: (data: CreateUserPayload) => usersApi.create(data),
29
+ onSuccess: () => {
30
+ queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] })
31
+ toast.success('User created')
32
+ },
33
+ onError: () => toast.error('Failed to create user'),
34
+ })
35
+ }
36
+
37
+ /** Update an existing user */
38
+ export function useUpdateUser() {
39
+ const queryClient = useQueryClient()
40
+ return useMutation({
41
+ mutationFn: ({ id, data }: { id: string; data: UpdateUserPayload }) => usersApi.update(id, data),
42
+ onSuccess: () => {
43
+ queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] })
44
+ toast.success('User updated')
45
+ },
46
+ onError: () => toast.error('Failed to update user'),
47
+ })
48
+ }
49
+
50
+ /** Delete a user by id */
51
+ export function useDeleteUser() {
52
+ const queryClient = useQueryClient()
53
+ return useMutation({
54
+ mutationFn: (id: string) => usersApi.delete(id),
55
+ onSuccess: () => {
56
+ queryClient.invalidateQueries({ queryKey: [USER_QUERY_KEY] })
57
+ toast.success('User deleted')
58
+ },
59
+ onError: () => toast.error('Failed to delete user'),
60
+ })
61
+ }
@@ -0,0 +1,32 @@
1
+ import apiClient from '@/lib/api-client'
2
+ import type { PaginatedResponse } from '@/types'
3
+ import type { User } from '../types/user'
4
+
5
+ export interface UserListParams {
6
+ page?: number
7
+ limit?: number
8
+ search?: string
9
+ role?: string
10
+ status?: string
11
+ }
12
+
13
+ export type CreateUserPayload = Omit<User, 'id' | 'createdAt'>
14
+ export type UpdateUserPayload = Partial<CreateUserPayload>
15
+
16
+ /** Raw API calls for the users resource — always use hooks (use-users.ts) in components */
17
+ export const usersApi = {
18
+ list: (params?: UserListParams): Promise<PaginatedResponse<User>> =>
19
+ apiClient.get('/users', { params }).then((r) => r.data),
20
+
21
+ getById: (id: string): Promise<User> =>
22
+ apiClient.get(`/users/${id}`).then((r) => r.data),
23
+
24
+ create: (data: CreateUserPayload): Promise<User> =>
25
+ apiClient.post('/users', data).then((r) => r.data),
26
+
27
+ update: (id: string, data: UpdateUserPayload): Promise<User> =>
28
+ apiClient.put(`/users/${id}`, data).then((r) => r.data),
29
+
30
+ delete: (id: string): Promise<void> =>
31
+ apiClient.delete(`/users/${id}`).then((r) => r.data),
32
+ }
@@ -1,3 +1,10 @@
1
+ /** Generic API response wrapper from backend */
2
+ export interface ApiResponse<T> {
3
+ data: T
4
+ message: string
5
+ success: boolean
6
+ }
7
+
1
8
  /** Generic paginated API response */
2
9
  export interface PaginatedResponse<T> {
3
10
  data: T[]