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 +5 -2
- package/templates/template-vite/.claude/commands/debug.md +24 -0
- package/templates/template-vite/.claude/commands/feature.md +31 -0
- package/templates/template-vite/.claude/commands/plan.md +30 -0
- package/templates/template-vite/.claude/commands/review.md +24 -0
- package/templates/template-vite/.husky/commit-msg +1 -0
- package/templates/template-vite/.husky/pre-commit +1 -0
- package/templates/template-vite/CLAUDE.md +121 -33
- package/templates/template-vite/_env.example +1 -2
- package/templates/template-vite/_package.json +5 -1
- package/templates/template-vite/commitlint.config.ts +3 -0
- package/templates/template-vite/src/features/tasks/components/tasks-crud-table.tsx +14 -23
- package/templates/template-vite/src/features/tasks/hooks/use-tasks.ts +61 -0
- package/templates/template-vite/src/features/tasks/services/tasks-api.ts +32 -0
- package/templates/template-vite/src/features/users/components/users-table.tsx +15 -32
- package/templates/template-vite/src/features/users/hooks/use-users.ts +61 -0
- package/templates/template-vite/src/features/users/services/users-api.ts +32 -0
- package/templates/template-vite/src/types/index.ts +7 -0
- /package/templates/template-vite/src/features/tasks/{data → services}/tasks-mock-data.ts +0 -0
- /package/templates/template-vite/src/features/users/{data → services}/users-mock-data.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doo-boilerplate",
|
|
3
|
-
"version": "0.
|
|
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": [
|
|
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
|
-
#
|
|
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
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- TanStack
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
27
|
-
pnpm build
|
|
28
|
-
pnpm
|
|
29
|
-
pnpm lint
|
|
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
|
-
##
|
|
33
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
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:`
|
|
@@ -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",
|
|
@@ -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 {
|
|
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
|
-
/**
|
|
36
|
+
/** CRUD data table for tasks — data fetched via TanStack Query, mutations invalidate cache */
|
|
38
37
|
export function TasksCrudTable() {
|
|
39
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
toast.success('Task updated')
|
|
56
|
+
updateTask.mutate({ id: editingTask.id, data: values })
|
|
62
57
|
} else {
|
|
63
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
File without changes
|
|
File without changes
|