blacksmith-cli 0.1.1
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/README.md +210 -0
- package/bin/blacksmith.js +20 -0
- package/dist/index.js +4404 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/templates/backend/.env.example.hbs +10 -0
- package/src/templates/backend/apps/__init__.py.hbs +0 -0
- package/src/templates/backend/apps/users/__init__.py.hbs +0 -0
- package/src/templates/backend/apps/users/admin.py.hbs +26 -0
- package/src/templates/backend/apps/users/managers.py.hbs +25 -0
- package/src/templates/backend/apps/users/models.py.hbs +25 -0
- package/src/templates/backend/apps/users/serializers.py.hbs +94 -0
- package/src/templates/backend/apps/users/tests.py.hbs +47 -0
- package/src/templates/backend/apps/users/urls.py.hbs +10 -0
- package/src/templates/backend/apps/users/views.py.hbs +175 -0
- package/src/templates/backend/config/__init__.py.hbs +0 -0
- package/src/templates/backend/config/asgi.py.hbs +9 -0
- package/src/templates/backend/config/settings/__init__.py.hbs +13 -0
- package/src/templates/backend/config/settings/base.py.hbs +117 -0
- package/src/templates/backend/config/settings/development.py.hbs +19 -0
- package/src/templates/backend/config/settings/production.py.hbs +31 -0
- package/src/templates/backend/config/urls.py.hbs +26 -0
- package/src/templates/backend/config/wsgi.py.hbs +9 -0
- package/src/templates/backend/manage.py.hbs +22 -0
- package/src/templates/backend/requirements.txt.hbs +7 -0
- package/src/templates/frontend/.env.hbs +1 -0
- package/src/templates/frontend/index.html.hbs +13 -0
- package/src/templates/frontend/openapi-ts.config.ts.hbs +29 -0
- package/src/templates/frontend/package.json.hbs +44 -0
- package/src/templates/frontend/postcss.config.js.hbs +6 -0
- package/src/templates/frontend/src/api/client.ts.hbs +110 -0
- package/src/templates/frontend/src/api/generated/.gitkeep +0 -0
- package/src/templates/frontend/src/api/generated/client.gen.ts +13 -0
- package/src/templates/frontend/src/api/query-client.ts.hbs +22 -0
- package/src/templates/frontend/src/app.tsx.hbs +30 -0
- package/src/templates/frontend/src/features/auth/adapter.ts.hbs +198 -0
- package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +32 -0
- package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +27 -0
- package/src/templates/frontend/src/features/auth/index.ts.hbs +3 -0
- package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +37 -0
- package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +36 -0
- package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +36 -0
- package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +41 -0
- package/src/templates/frontend/src/features/auth/routes.tsx.hbs +13 -0
- package/src/templates/frontend/src/main.tsx.hbs +10 -0
- package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +36 -0
- package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +69 -0
- package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +14 -0
- package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +21 -0
- package/src/templates/frontend/src/pages/dashboard/index.ts.hbs +1 -0
- package/src/templates/frontend/src/pages/dashboard/routes.tsx.hbs +7 -0
- package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +88 -0
- package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +88 -0
- package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +47 -0
- package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +34 -0
- package/src/templates/frontend/src/pages/home/home.tsx.hbs +20 -0
- package/src/templates/frontend/src/pages/home/index.ts.hbs +1 -0
- package/src/templates/frontend/src/pages/home/routes.tsx.hbs +7 -0
- package/src/templates/frontend/src/router/auth-guard.tsx.hbs +57 -0
- package/src/templates/frontend/src/router/error-boundary.tsx.hbs +61 -0
- package/src/templates/frontend/src/router/index.tsx.hbs +12 -0
- package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +68 -0
- package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +137 -0
- package/src/templates/frontend/src/router/paths.ts.hbs +38 -0
- package/src/templates/frontend/src/router/routes.tsx.hbs +64 -0
- package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +20 -0
- package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +31 -0
- package/src/templates/frontend/src/shared/hooks/api-error.ts.hbs +147 -0
- package/src/templates/frontend/src/shared/hooks/use-api-mutation.ts.hbs +88 -0
- package/src/templates/frontend/src/shared/hooks/use-api-query.ts.hbs +66 -0
- package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +10 -0
- package/src/templates/frontend/src/styles/globals.css.hbs +62 -0
- package/src/templates/frontend/src/vite-env.d.ts.hbs +1 -0
- package/src/templates/frontend/tailwind.config.js.hbs +73 -0
- package/src/templates/frontend/tsconfig.app.json.hbs +25 -0
- package/src/templates/frontend/tsconfig.json.hbs +7 -0
- package/src/templates/frontend/tsconfig.node.json.hbs +18 -0
- package/src/templates/frontend/vite.config.ts.hbs +21 -0
- package/src/templates/resource/backend/__init__.py.hbs +0 -0
- package/src/templates/resource/backend/admin.py.hbs +10 -0
- package/src/templates/resource/backend/models.py.hbs +24 -0
- package/src/templates/resource/backend/serializers.py.hbs +21 -0
- package/src/templates/resource/backend/tests.py.hbs +35 -0
- package/src/templates/resource/backend/urls.py.hbs +10 -0
- package/src/templates/resource/backend/views.py.hbs +32 -0
- package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +39 -0
- package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +106 -0
- package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +49 -0
- package/src/templates/resource/frontend/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
- package/src/templates/resource/frontend/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
- package/src/templates/resource/frontend/index.ts.hbs +6 -0
- package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +33 -0
- package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
- package/src/templates/resource/frontend/routes.tsx.hbs +15 -0
- package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +39 -0
- package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +106 -0
- package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +49 -0
- package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
- package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
- package/src/templates/resource/pages/index.ts.hbs +6 -0
- package/src/templates/resource/pages/routes.tsx.hbs +15 -0
- package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +33 -0
- package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Detail Page
|
|
3
|
+
*
|
|
4
|
+
* Shows a single {{name}} with error handling.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
9
|
+
import { useApiQuery } from '@/shared/hooks/use-api-query'
|
|
10
|
+
import { useDelete{{Name}} } from '../hooks/use-{{kebab}}-mutations'
|
|
11
|
+
import { Alert, AlertDescription } from '@blacksmith-ui/react'
|
|
12
|
+
import { Path } from '@/router/paths'
|
|
13
|
+
import {
|
|
14
|
+
{{snakes}}RetrieveOptions,
|
|
15
|
+
} from '@/api/generated/@tanstack/react-query.gen'
|
|
16
|
+
|
|
17
|
+
export default function {{Name}}DetailPage() {
|
|
18
|
+
const { id } = useParams<{ id: string }>()
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
const delete{{Name}} = useDelete{{Name}}()
|
|
21
|
+
|
|
22
|
+
const { data: {{name}}, isLoading, errorMessage } = useApiQuery({
|
|
23
|
+
...{{snakes}}RetrieveOptions({
|
|
24
|
+
path: { id: Number(id) },
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="animate-pulse space-y-4">
|
|
31
|
+
<div className="h-8 w-64 bg-muted rounded" />
|
|
32
|
+
<div className="h-4 w-full bg-muted rounded" />
|
|
33
|
+
<div className="h-4 w-3/4 bg-muted rounded" />
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (errorMessage) {
|
|
39
|
+
return (
|
|
40
|
+
<Alert variant="destructive">
|
|
41
|
+
<AlertDescription>{errorMessage}</AlertDescription>
|
|
42
|
+
</Alert>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!{{name}}) {
|
|
47
|
+
return <div className="text-muted-foreground">{{Name}} not found.</div>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleDelete = async () => {
|
|
51
|
+
if (window.confirm('Are you sure you want to delete this {{name}}?')) {
|
|
52
|
+
await delete{{Name}}.mutateAsync({ path: { id: Number(id) } })
|
|
53
|
+
navigate(Path.{{Names}})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
{delete{{Name}}.errorMessage && (
|
|
60
|
+
<Alert variant="destructive" className="mb-4">
|
|
61
|
+
<AlertDescription>{delete{{Name}}.errorMessage}</AlertDescription>
|
|
62
|
+
</Alert>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<div className="flex items-center justify-between mb-6">
|
|
66
|
+
<h1 className="text-2xl font-bold">
|
|
67
|
+
{({{name}} as any).title}
|
|
68
|
+
</h1>
|
|
69
|
+
<div className="flex gap-2">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => navigate(`${Path.{{Names}}}/${id}/edit`)}
|
|
72
|
+
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-muted"
|
|
73
|
+
>
|
|
74
|
+
Edit
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleDelete}
|
|
78
|
+
disabled={delete{{Name}}.isPending}
|
|
79
|
+
className="px-4 py-2 text-sm text-destructive border border-destructive/30 rounded-md hover:bg-destructive/10 disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{delete{{Name}}.isPending ? 'Deleting...' : 'Delete'}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="bg-card rounded-lg border border-border p-6">
|
|
87
|
+
<p className="text-card-foreground whitespace-pre-wrap">
|
|
88
|
+
{({{name}} as any).description || 'No description.'}
|
|
89
|
+
</p>
|
|
90
|
+
<p className="mt-4 text-sm text-muted-foreground">
|
|
91
|
+
Created {new Date(({{name}} as any).created_at).toLocaleString()}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Outlet, type RouteObject } from 'react-router-dom'
|
|
2
|
+
import { Path } from '@/router/paths'
|
|
3
|
+
import {{Names}}Page from './pages/{{kebabs}}-page'
|
|
4
|
+
import {{Name}}DetailPage from './pages/{{kebab}}-detail-page'
|
|
5
|
+
|
|
6
|
+
export const {{names}}Routes: RouteObject[] = [
|
|
7
|
+
{
|
|
8
|
+
path: Path.{{Names}},
|
|
9
|
+
element: <Outlet />,
|
|
10
|
+
children: [
|
|
11
|
+
{ index: true, element: <{{Names}}Page /> },
|
|
12
|
+
{ path: ':id', element: <{{Name}}DetailPage /> },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Card Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single {{name}} in a card format.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Link } from 'react-router-dom'
|
|
9
|
+
import { Path } from '@/router/paths'
|
|
10
|
+
|
|
11
|
+
interface {{Name}}CardProps {
|
|
12
|
+
{{name}}: {
|
|
13
|
+
id: number
|
|
14
|
+
title: string
|
|
15
|
+
description?: string
|
|
16
|
+
created_at: string
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function {{Name}}Card({ {{name}} }: {{Name}}CardProps) {
|
|
21
|
+
return (
|
|
22
|
+
<Link
|
|
23
|
+
to={`${Path.{{Names}}}/${ {{name}}.id}`}
|
|
24
|
+
className="block p-6 bg-white rounded-lg border border-gray-200 hover:border-blue-300 hover:shadow-sm transition-all"
|
|
25
|
+
>
|
|
26
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
27
|
+
{ {{name}}.title}
|
|
28
|
+
</h3>
|
|
29
|
+
{ {{name}}.description && (
|
|
30
|
+
<p className="mt-2 text-sm text-gray-600 line-clamp-2">
|
|
31
|
+
{ {{name}}.description}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
<p className="mt-3 text-xs text-gray-400">
|
|
35
|
+
{new Date({{name}}.created_at).toLocaleDateString()}
|
|
36
|
+
</p>
|
|
37
|
+
</Link>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Form Component
|
|
3
|
+
*
|
|
4
|
+
* Form for creating and editing {{names}}.
|
|
5
|
+
* Supports both client-side (Zod) and server-side (DRF) field errors.
|
|
6
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useForm } from 'react-hook-form'
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
12
|
+
import { Alert, AlertDescription } from '@blacksmith-ui/react'
|
|
13
|
+
|
|
14
|
+
const {{name}}Schema = z.object({
|
|
15
|
+
title: z.string().min(1, 'Title is required').max(255),
|
|
16
|
+
description: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
type {{Name}}FormData = z.infer<typeof {{name}}Schema>
|
|
20
|
+
|
|
21
|
+
interface {{Name}}FormProps {
|
|
22
|
+
defaultValues?: Partial<{{Name}}FormData>
|
|
23
|
+
onSubmit: (data: {{Name}}FormData) => void | Promise<void>
|
|
24
|
+
isSubmitting?: boolean
|
|
25
|
+
/** General error message (e.g. from useApiMutation's errorMessage) */
|
|
26
|
+
errorMessage?: string | null
|
|
27
|
+
/** Server-side field errors (e.g. from useApiMutation's fieldErrors) */
|
|
28
|
+
fieldErrors?: Record<string, string[]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function {{Name}}Form({
|
|
32
|
+
defaultValues,
|
|
33
|
+
onSubmit,
|
|
34
|
+
isSubmitting,
|
|
35
|
+
errorMessage,
|
|
36
|
+
fieldErrors = {},
|
|
37
|
+
}: {{Name}}FormProps) {
|
|
38
|
+
const form = useForm<{{Name}}FormData>({
|
|
39
|
+
resolver: zodResolver({{name}}Schema),
|
|
40
|
+
defaultValues: {
|
|
41
|
+
title: '',
|
|
42
|
+
description: '',
|
|
43
|
+
...defaultValues,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const getFieldError = (field: string): string | undefined => {
|
|
48
|
+
// Client-side errors take priority
|
|
49
|
+
const clientError = form.formState.errors[field as keyof {{Name}}FormData]?.message
|
|
50
|
+
if (clientError) return clientError
|
|
51
|
+
// Fall back to server-side errors
|
|
52
|
+
return fieldErrors[field]?.[0]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
57
|
+
{errorMessage && (
|
|
58
|
+
<Alert variant="destructive">
|
|
59
|
+
<AlertDescription>{errorMessage}</AlertDescription>
|
|
60
|
+
</Alert>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<div>
|
|
64
|
+
<label htmlFor="title" className="block text-sm font-medium">
|
|
65
|
+
Title
|
|
66
|
+
</label>
|
|
67
|
+
<input
|
|
68
|
+
{...form.register('title')}
|
|
69
|
+
id="title"
|
|
70
|
+
type="text"
|
|
71
|
+
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:border-ring focus:outline-none focus:ring-1 focus:ring-ring"
|
|
72
|
+
/>
|
|
73
|
+
{getFieldError('title') && (
|
|
74
|
+
<p className="mt-1 text-sm text-destructive">
|
|
75
|
+
{getFieldError('title')}
|
|
76
|
+
</p>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div>
|
|
81
|
+
<label htmlFor="description" className="block text-sm font-medium">
|
|
82
|
+
Description
|
|
83
|
+
</label>
|
|
84
|
+
<textarea
|
|
85
|
+
{...form.register('description')}
|
|
86
|
+
id="description"
|
|
87
|
+
rows={4}
|
|
88
|
+
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:border-ring focus:outline-none focus:ring-1 focus:ring-ring"
|
|
89
|
+
/>
|
|
90
|
+
{getFieldError('description') && (
|
|
91
|
+
<p className="mt-1 text-sm text-destructive">
|
|
92
|
+
{getFieldError('description')}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<button
|
|
98
|
+
type="submit"
|
|
99
|
+
disabled={isSubmitting}
|
|
100
|
+
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
|
|
101
|
+
>
|
|
102
|
+
{isSubmitting ? 'Saving...' : 'Save'}
|
|
103
|
+
</button>
|
|
104
|
+
</form>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} List Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a list of {{names}} with loading and empty states.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { {{Name}}Card } from './{{kebab}}-card'
|
|
9
|
+
|
|
10
|
+
interface {{Name}}ListProps {
|
|
11
|
+
{{names}}: Array<{
|
|
12
|
+
id: number
|
|
13
|
+
title: string
|
|
14
|
+
description?: string
|
|
15
|
+
created_at: string
|
|
16
|
+
}>
|
|
17
|
+
isLoading?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function {{Name}}List({ {{names}}, isLoading }: {{Name}}ListProps) {
|
|
21
|
+
if (isLoading) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
24
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
25
|
+
<div
|
|
26
|
+
key={i}
|
|
27
|
+
className="h-32 bg-gray-100 rounded-lg animate-pulse"
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if ({{names}}.length === 0) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="text-center py-12">
|
|
37
|
+
<p className="text-gray-500">No {{names}} yet. Create your first one!</p>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
44
|
+
{ {{names}}.map(({{name}}) => (
|
|
45
|
+
<{{Name}}Card key={ {{name}}.id} {{name}}={ {{name}}} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* use{{Names}} Hook
|
|
3
|
+
*
|
|
4
|
+
* Wraps the generated list query with smart retry and error parsing.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useApiQuery } from '@/shared/hooks/use-api-query'
|
|
9
|
+
import {
|
|
10
|
+
{{snakes}}ListOptions,
|
|
11
|
+
} from '@/api/generated/@tanstack/react-query.gen'
|
|
12
|
+
|
|
13
|
+
interface Use{{Names}}Params {
|
|
14
|
+
page?: number
|
|
15
|
+
search?: string
|
|
16
|
+
ordering?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function use{{Names}}(params: Use{{Names}}Params = {}) {
|
|
20
|
+
return useApiQuery({
|
|
21
|
+
...{{snakes}}ListOptions({
|
|
22
|
+
query: {
|
|
23
|
+
page: params.page ?? 1,
|
|
24
|
+
search: params.search,
|
|
25
|
+
ordering: params.ordering ?? '-created_at',
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
select: (data: any) => ({
|
|
29
|
+
{{names}}: data.results ?? [],
|
|
30
|
+
total: data.count ?? 0,
|
|
31
|
+
hasNext: !!data.next,
|
|
32
|
+
hasPrev: !!data.previous,
|
|
33
|
+
}),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Mutation Hooks
|
|
3
|
+
*
|
|
4
|
+
* Create, update, and delete with cache invalidation and error parsing.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useApiMutation } from '@/shared/hooks/use-api-mutation'
|
|
9
|
+
import {
|
|
10
|
+
{{snakes}}CreateMutation,
|
|
11
|
+
{{snakes}}UpdateMutation,
|
|
12
|
+
{{snakes}}DestroyMutation,
|
|
13
|
+
{{snakes}}ListQueryKey,
|
|
14
|
+
{{snakes}}RetrieveQueryKey,
|
|
15
|
+
} from '@/api/generated/@tanstack/react-query.gen'
|
|
16
|
+
|
|
17
|
+
export function useCreate{{Name}}() {
|
|
18
|
+
return useApiMutation({
|
|
19
|
+
...{{snakes}}CreateMutation(),
|
|
20
|
+
invalidateKeys: [{{snakes}}ListQueryKey()],
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useUpdate{{Name}}(id: number) {
|
|
25
|
+
return useApiMutation({
|
|
26
|
+
...{{snakes}}UpdateMutation(),
|
|
27
|
+
invalidateKeys: [
|
|
28
|
+
{{snakes}}ListQueryKey(),
|
|
29
|
+
{{snakes}}RetrieveQueryKey({ path: { id } }),
|
|
30
|
+
],
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useDelete{{Name}}() {
|
|
35
|
+
return useApiMutation({
|
|
36
|
+
...{{snakes}}DestroyMutation(),
|
|
37
|
+
invalidateKeys: [{{snakes}}ListQueryKey()],
|
|
38
|
+
})
|
|
39
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { {{names}}Routes } from './routes'
|
|
2
|
+
export { use{{Names}} } from './hooks/use-{{kebabs}}-query'
|
|
3
|
+
export { useCreate{{Name}}, useUpdate{{Name}}, useDelete{{Name}} } from './hooks/use-{{kebab}}-mutations'
|
|
4
|
+
export { {{Name}}Card } from './components/{{kebab}}-card'
|
|
5
|
+
export { {{Name}}List } from './components/{{kebab}}-list'
|
|
6
|
+
export { {{Name}}Form } from './components/{{kebab}}-form'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Outlet, type RouteObject } from 'react-router-dom'
|
|
2
|
+
import { Path } from '@/router/paths'
|
|
3
|
+
import {{Names}}Page from './{{kebabs}}-page'
|
|
4
|
+
import {{Name}}DetailPage from './{{kebab}}-detail-page'
|
|
5
|
+
|
|
6
|
+
export const {{names}}Routes: RouteObject[] = [
|
|
7
|
+
{
|
|
8
|
+
path: Path.{{Names}},
|
|
9
|
+
element: <Outlet />,
|
|
10
|
+
children: [
|
|
11
|
+
{ index: true, element: <{{Names}}Page /> },
|
|
12
|
+
{ path: ':id', element: <{{Name}}DetailPage /> },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Names}} List Page
|
|
3
|
+
*
|
|
4
|
+
* Lists all {{names}} with loading and error states.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { use{{Names}} } from './hooks/use-{{kebabs}}-query'
|
|
9
|
+
import { {{Name}}List } from './components/{{kebab}}-list'
|
|
10
|
+
import { Alert, AlertDescription } from '@blacksmith-ui/react'
|
|
11
|
+
|
|
12
|
+
export default function {{Names}}Page() {
|
|
13
|
+
const { data, isLoading, errorMessage } = use{{Names}}()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div>
|
|
17
|
+
<div className="flex items-center justify-between mb-6">
|
|
18
|
+
<h1 className="text-2xl font-bold">{{Names}}</h1>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{errorMessage && (
|
|
22
|
+
<Alert variant="destructive" className="mb-4">
|
|
23
|
+
<AlertDescription>{errorMessage}</AlertDescription>
|
|
24
|
+
</Alert>
|
|
25
|
+
)}
|
|
26
|
+
|
|
27
|
+
<{{Name}}List
|
|
28
|
+
{{names}}={data?.{{names}} ?? []}
|
|
29
|
+
isLoading={isLoading}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Detail Page
|
|
3
|
+
*
|
|
4
|
+
* Shows a single {{name}} with error handling.
|
|
5
|
+
* Generated by Blacksmith. You own this file — customize as needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
9
|
+
import { useApiQuery } from '@/shared/hooks/use-api-query'
|
|
10
|
+
import { useDelete{{Name}} } from './hooks/use-{{kebab}}-mutations'
|
|
11
|
+
import { Alert, AlertDescription } from '@blacksmith-ui/react'
|
|
12
|
+
import { Path } from '@/router/paths'
|
|
13
|
+
import {
|
|
14
|
+
{{snakes}}RetrieveOptions,
|
|
15
|
+
} from '@/api/generated/@tanstack/react-query.gen'
|
|
16
|
+
|
|
17
|
+
export default function {{Name}}DetailPage() {
|
|
18
|
+
const { id } = useParams<{ id: string }>()
|
|
19
|
+
const navigate = useNavigate()
|
|
20
|
+
const delete{{Name}} = useDelete{{Name}}()
|
|
21
|
+
|
|
22
|
+
const { data: {{name}}, isLoading, errorMessage } = useApiQuery({
|
|
23
|
+
...{{snakes}}RetrieveOptions({
|
|
24
|
+
path: { id: Number(id) },
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="animate-pulse space-y-4">
|
|
31
|
+
<div className="h-8 w-64 bg-muted rounded" />
|
|
32
|
+
<div className="h-4 w-full bg-muted rounded" />
|
|
33
|
+
<div className="h-4 w-3/4 bg-muted rounded" />
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (errorMessage) {
|
|
39
|
+
return (
|
|
40
|
+
<Alert variant="destructive">
|
|
41
|
+
<AlertDescription>{errorMessage}</AlertDescription>
|
|
42
|
+
</Alert>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!{{name}}) {
|
|
47
|
+
return <div className="text-muted-foreground">{{Name}} not found.</div>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleDelete = async () => {
|
|
51
|
+
if (window.confirm('Are you sure you want to delete this {{name}}?')) {
|
|
52
|
+
await delete{{Name}}.mutateAsync({ path: { id: Number(id) } })
|
|
53
|
+
navigate(Path.{{Names}})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
{delete{{Name}}.errorMessage && (
|
|
60
|
+
<Alert variant="destructive" className="mb-4">
|
|
61
|
+
<AlertDescription>{delete{{Name}}.errorMessage}</AlertDescription>
|
|
62
|
+
</Alert>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<div className="flex items-center justify-between mb-6">
|
|
66
|
+
<h1 className="text-2xl font-bold">
|
|
67
|
+
{({{name}} as any).title}
|
|
68
|
+
</h1>
|
|
69
|
+
<div className="flex gap-2">
|
|
70
|
+
<button
|
|
71
|
+
onClick={() => navigate(`${Path.{{Names}}}/${id}/edit`)}
|
|
72
|
+
className="px-4 py-2 text-sm border border-border rounded-md hover:bg-muted"
|
|
73
|
+
>
|
|
74
|
+
Edit
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={handleDelete}
|
|
78
|
+
disabled={delete{{Name}}.isPending}
|
|
79
|
+
className="px-4 py-2 text-sm text-destructive border border-destructive/30 rounded-md hover:bg-destructive/10 disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{delete{{Name}}.isPending ? 'Deleting...' : 'Delete'}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="bg-card rounded-lg border border-border p-6">
|
|
87
|
+
<p className="text-card-foreground whitespace-pre-wrap">
|
|
88
|
+
{({{name}} as any).description || 'No description.'}
|
|
89
|
+
</p>
|
|
90
|
+
<p className="mt-4 text-sm text-muted-foreground">
|
|
91
|
+
Created {new Date(({{name}} as any).created_at).toLocaleString()}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|