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.
Files changed (103) hide show
  1. package/README.md +210 -0
  2. package/bin/blacksmith.js +20 -0
  3. package/dist/index.js +4404 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +51 -0
  6. package/src/templates/backend/.env.example.hbs +10 -0
  7. package/src/templates/backend/apps/__init__.py.hbs +0 -0
  8. package/src/templates/backend/apps/users/__init__.py.hbs +0 -0
  9. package/src/templates/backend/apps/users/admin.py.hbs +26 -0
  10. package/src/templates/backend/apps/users/managers.py.hbs +25 -0
  11. package/src/templates/backend/apps/users/models.py.hbs +25 -0
  12. package/src/templates/backend/apps/users/serializers.py.hbs +94 -0
  13. package/src/templates/backend/apps/users/tests.py.hbs +47 -0
  14. package/src/templates/backend/apps/users/urls.py.hbs +10 -0
  15. package/src/templates/backend/apps/users/views.py.hbs +175 -0
  16. package/src/templates/backend/config/__init__.py.hbs +0 -0
  17. package/src/templates/backend/config/asgi.py.hbs +9 -0
  18. package/src/templates/backend/config/settings/__init__.py.hbs +13 -0
  19. package/src/templates/backend/config/settings/base.py.hbs +117 -0
  20. package/src/templates/backend/config/settings/development.py.hbs +19 -0
  21. package/src/templates/backend/config/settings/production.py.hbs +31 -0
  22. package/src/templates/backend/config/urls.py.hbs +26 -0
  23. package/src/templates/backend/config/wsgi.py.hbs +9 -0
  24. package/src/templates/backend/manage.py.hbs +22 -0
  25. package/src/templates/backend/requirements.txt.hbs +7 -0
  26. package/src/templates/frontend/.env.hbs +1 -0
  27. package/src/templates/frontend/index.html.hbs +13 -0
  28. package/src/templates/frontend/openapi-ts.config.ts.hbs +29 -0
  29. package/src/templates/frontend/package.json.hbs +44 -0
  30. package/src/templates/frontend/postcss.config.js.hbs +6 -0
  31. package/src/templates/frontend/src/api/client.ts.hbs +110 -0
  32. package/src/templates/frontend/src/api/generated/.gitkeep +0 -0
  33. package/src/templates/frontend/src/api/generated/client.gen.ts +13 -0
  34. package/src/templates/frontend/src/api/query-client.ts.hbs +22 -0
  35. package/src/templates/frontend/src/app.tsx.hbs +30 -0
  36. package/src/templates/frontend/src/features/auth/adapter.ts.hbs +198 -0
  37. package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +32 -0
  38. package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +27 -0
  39. package/src/templates/frontend/src/features/auth/index.ts.hbs +3 -0
  40. package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +37 -0
  41. package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +36 -0
  42. package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +36 -0
  43. package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +41 -0
  44. package/src/templates/frontend/src/features/auth/routes.tsx.hbs +13 -0
  45. package/src/templates/frontend/src/main.tsx.hbs +10 -0
  46. package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +36 -0
  47. package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +69 -0
  48. package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +14 -0
  49. package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +21 -0
  50. package/src/templates/frontend/src/pages/dashboard/index.ts.hbs +1 -0
  51. package/src/templates/frontend/src/pages/dashboard/routes.tsx.hbs +7 -0
  52. package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +88 -0
  53. package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +88 -0
  54. package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +47 -0
  55. package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +34 -0
  56. package/src/templates/frontend/src/pages/home/home.tsx.hbs +20 -0
  57. package/src/templates/frontend/src/pages/home/index.ts.hbs +1 -0
  58. package/src/templates/frontend/src/pages/home/routes.tsx.hbs +7 -0
  59. package/src/templates/frontend/src/router/auth-guard.tsx.hbs +57 -0
  60. package/src/templates/frontend/src/router/error-boundary.tsx.hbs +61 -0
  61. package/src/templates/frontend/src/router/index.tsx.hbs +12 -0
  62. package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +68 -0
  63. package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +137 -0
  64. package/src/templates/frontend/src/router/paths.ts.hbs +38 -0
  65. package/src/templates/frontend/src/router/routes.tsx.hbs +64 -0
  66. package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +20 -0
  67. package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +31 -0
  68. package/src/templates/frontend/src/shared/hooks/api-error.ts.hbs +147 -0
  69. package/src/templates/frontend/src/shared/hooks/use-api-mutation.ts.hbs +88 -0
  70. package/src/templates/frontend/src/shared/hooks/use-api-query.ts.hbs +66 -0
  71. package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +10 -0
  72. package/src/templates/frontend/src/styles/globals.css.hbs +62 -0
  73. package/src/templates/frontend/src/vite-env.d.ts.hbs +1 -0
  74. package/src/templates/frontend/tailwind.config.js.hbs +73 -0
  75. package/src/templates/frontend/tsconfig.app.json.hbs +25 -0
  76. package/src/templates/frontend/tsconfig.json.hbs +7 -0
  77. package/src/templates/frontend/tsconfig.node.json.hbs +18 -0
  78. package/src/templates/frontend/vite.config.ts.hbs +21 -0
  79. package/src/templates/resource/backend/__init__.py.hbs +0 -0
  80. package/src/templates/resource/backend/admin.py.hbs +10 -0
  81. package/src/templates/resource/backend/models.py.hbs +24 -0
  82. package/src/templates/resource/backend/serializers.py.hbs +21 -0
  83. package/src/templates/resource/backend/tests.py.hbs +35 -0
  84. package/src/templates/resource/backend/urls.py.hbs +10 -0
  85. package/src/templates/resource/backend/views.py.hbs +32 -0
  86. package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +39 -0
  87. package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +106 -0
  88. package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +49 -0
  89. package/src/templates/resource/frontend/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  90. package/src/templates/resource/frontend/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  91. package/src/templates/resource/frontend/index.ts.hbs +6 -0
  92. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +33 -0
  93. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +96 -0
  94. package/src/templates/resource/frontend/routes.tsx.hbs +15 -0
  95. package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +39 -0
  96. package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +106 -0
  97. package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +49 -0
  98. package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +35 -0
  99. package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +39 -0
  100. package/src/templates/resource/pages/index.ts.hbs +6 -0
  101. package/src/templates/resource/pages/routes.tsx.hbs +15 -0
  102. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +33 -0
  103. 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
+ }