blacksmith-cli 0.1.4 → 0.1.6

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 (32) hide show
  1. package/dist/index.js +276 -20
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/src/templates/backend/utils/__init__.py.hbs +0 -0
  5. package/src/templates/backend/utils/models.py.hbs +11 -0
  6. package/src/templates/frontend/package.json.hbs +9 -1
  7. package/src/templates/frontend/src/__tests__/setup.ts.hbs +21 -0
  8. package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +80 -0
  9. package/src/templates/frontend/src/pages/home/home.tsx.hbs +93 -11
  10. package/src/templates/frontend/tsconfig.app.json.hbs +1 -0
  11. package/src/templates/frontend/vite.config.ts.hbs +8 -0
  12. package/src/templates/resource/api-hooks/index.ts.hbs +2 -0
  13. package/src/templates/resource/{frontend/hooks → api-hooks}/use-{{kebabs}}-query.ts.hbs +10 -2
  14. package/src/templates/resource/{frontend/hooks → api-hooks}/use-{{kebab}}-mutations.ts.hbs +1 -1
  15. package/src/templates/resource/backend/models.py.hbs +2 -3
  16. package/src/templates/resource/frontend/components/{{kebab}}-card.tsx.hbs +1 -1
  17. package/src/templates/resource/frontend/components/{{kebab}}-list.tsx.hbs +1 -1
  18. package/src/templates/resource/frontend/index.ts.hbs +1 -2
  19. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +1 -1
  20. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +3 -11
  21. package/src/templates/resource/pages/components/{{kebab}}-card.tsx.hbs +1 -1
  22. package/src/templates/resource/pages/components/{{kebab}}-list.tsx.hbs +1 -1
  23. package/src/templates/resource/pages/hooks/index.ts.hbs +9 -0
  24. package/src/templates/resource/pages/index.ts.hbs +1 -2
  25. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +1 -1
  26. package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +3 -11
  27. package/src/templates/frontend/src/pages/home/components/features-grid.tsx.hbs +0 -88
  28. package/src/templates/frontend/src/pages/home/components/getting-started.tsx.hbs +0 -88
  29. package/src/templates/frontend/src/pages/home/components/hero-section.tsx.hbs +0 -47
  30. package/src/templates/frontend/src/pages/home/components/resources-section.tsx.hbs +0 -34
  31. package/src/templates/resource/pages/hooks/use-{{kebabs}}-query.ts.hbs +0 -35
  32. package/src/templates/resource/pages/hooks/use-{{kebab}}-mutations.ts.hbs +0 -39
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blacksmith-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Fullstack Django + React framework — one command, one codebase, one mental model",
5
5
  "type": "module",
6
6
  "bin": {
File without changes
@@ -0,0 +1,11 @@
1
+ import uuid
2
+ from django.db import models
3
+
4
+
5
+ class BaseModel(models.Model):
6
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
7
+ created_at = models.DateTimeField(auto_now_add=True)
8
+ updated_at = models.DateTimeField(auto_now=True)
9
+
10
+ class Meta:
11
+ abstract = True
@@ -8,6 +8,9 @@
8
8
  "build": "tsc -b && vite build",
9
9
  "lint": "eslint .",
10
10
  "preview": "vite preview",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
11
14
  "openapi-ts": "openapi-ts"
12
15
  },
13
16
  "dependencies": {
@@ -32,13 +35,18 @@
32
35
  },
33
36
  "devDependencies": {
34
37
  "@hey-api/openapi-ts": "^0.93.0",
38
+ "@testing-library/jest-dom": "^6.6.0",
39
+ "@testing-library/react": "^16.1.0",
40
+ "@testing-library/user-event": "^14.5.0",
35
41
  "@types/react": "^19.1.0",
36
42
  "@types/react-dom": "^19.1.0",
37
43
  "@vitejs/plugin-react": "^5.0.0",
38
44
  "autoprefixer": "^10.4.0",
45
+ "jsdom": "^25.0.0",
39
46
  "postcss": "^8.4.0",
40
47
  "tailwindcss-animate": "^1.0.7",
41
48
  "typescript": "~5.8.0",
42
- "vite": "^6.3.0"
49
+ "vite": "^6.3.0",
50
+ "vitest": "^3.0.0"
43
51
  }
44
52
  }
@@ -0,0 +1,21 @@
1
+ import '@testing-library/jest-dom/vitest'
2
+
3
+ // Mock window.matchMedia (not implemented in jsdom)
4
+ Object.defineProperty(window, 'matchMedia', {
5
+ writable: true,
6
+ value: vi.fn().mockImplementation((query: string) => ({
7
+ matches: false,
8
+ media: query,
9
+ onchange: null,
10
+ addListener: vi.fn(),
11
+ removeListener: vi.fn(),
12
+ addEventListener: vi.fn(),
13
+ removeEventListener: vi.fn(),
14
+ dispatchEvent: vi.fn(),
15
+ })),
16
+ })
17
+
18
+ // Clean up after each test
19
+ afterEach(() => {
20
+ document.body.innerHTML = ''
21
+ })
@@ -0,0 +1,80 @@
1
+ import { type ReactElement, type ReactNode } from 'react'
2
+ import { render, type RenderOptions } from '@testing-library/react'
3
+ import userEvent from '@testing-library/user-event'
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
+ import { ThemeProvider } from '@blacksmith-ui/react'
6
+ import { MemoryRouter } from 'react-router-dom'
7
+
8
+ /**
9
+ * Creates a fresh QueryClient configured for tests.
10
+ * Disables retries and garbage collection to keep tests fast and deterministic.
11
+ */
12
+ function createTestQueryClient() {
13
+ return new QueryClient({
14
+ defaultOptions: {
15
+ queries: {
16
+ retry: false,
17
+ gcTime: 0,
18
+ },
19
+ mutations: {
20
+ retry: false,
21
+ },
22
+ },
23
+ })
24
+ }
25
+
26
+ interface WrapperProps {
27
+ children: ReactNode
28
+ }
29
+
30
+ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
31
+ /** Initial route entries for MemoryRouter (defaults to ["/"]) */
32
+ routerEntries?: string[]
33
+ /** Provide your own QueryClient (a fresh one is created by default) */
34
+ queryClient?: QueryClient
35
+ }
36
+
37
+ /**
38
+ * Custom render that wraps components with all app providers:
39
+ * - ThemeProvider (light mode for consistent snapshots)
40
+ * - QueryClientProvider (with test-friendly defaults)
41
+ * - MemoryRouter (for components that use routing hooks)
42
+ *
43
+ * Also returns a `user` instance from @testing-library/user-event.
44
+ *
45
+ * @example
46
+ * const { user } = renderWithProviders(<MyComponent />)
47
+ * await user.click(screen.getByRole('button'))
48
+ */
49
+ export function renderWithProviders(
50
+ ui: ReactElement,
51
+ {
52
+ routerEntries = ['/'],
53
+ queryClient = createTestQueryClient(),
54
+ ...renderOptions
55
+ }: CustomRenderOptions = {},
56
+ ) {
57
+ function Wrapper({ children }: WrapperProps) {
58
+ return (
59
+ <ThemeProvider defaultMode="light" storageKey="test-theme">
60
+ <QueryClientProvider client={queryClient}>
61
+ <MemoryRouter initialEntries={routerEntries}>
62
+ {children}
63
+ </MemoryRouter>
64
+ </QueryClientProvider>
65
+ </ThemeProvider>
66
+ )
67
+ }
68
+
69
+ return {
70
+ user: userEvent.setup(),
71
+ ...render(ui, { wrapper: Wrapper, ...renderOptions }),
72
+ }
73
+ }
74
+
75
+ export { createTestQueryClient }
76
+
77
+ // Re-export everything from @testing-library/react for convenience
78
+ export * from '@testing-library/react'
79
+ // Override render with our custom version
80
+ export { renderWithProviders as render }
@@ -1,20 +1,102 @@
1
- import { Stack, Divider } from '@blacksmith-ui/react'
1
+ import {
2
+ Stack,
3
+ Flex,
4
+ Typography,
5
+ Text,
6
+ Button,
7
+ Card,
8
+ CardHeader,
9
+ CardTitle,
10
+ CardDescription,
11
+ CardContent,
12
+ Grid,
13
+ } from '@blacksmith-ui/react'
2
14
  import { useAuth } from '@/features/auth/hooks/use-auth'
3
- import { HeroSection } from './components/hero-section'
4
- import { GettingStarted } from './components/getting-started'
5
- import { FeaturesGrid } from './components/features-grid'
6
- import { ResourcesSection } from './components/resources-section'
15
+ import { Link } from 'react-router-dom'
16
+ import { Path } from '@/router/paths'
17
+ import { BookOpen, Terminal, Layout } from 'lucide-react'
18
+
19
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
7
20
 
8
21
  export default function HomePage() {
9
22
  const { isAuthenticated } = useAuth()
10
23
 
11
24
  return (
12
- <Stack gap={12}>
13
- <HeroSection isAuthenticated={isAuthenticated} />
14
- <Divider />
15
- <GettingStarted />
16
- <FeaturesGrid />
17
- <ResourcesSection />
25
+ <Stack gap={10} className="pt-16 pb-12">
26
+ <Stack gap={4} align="center">
27
+ <Typography variant="h1" className="sm:text-5xl">
28
+ {{projectName}}
29
+ </Typography>
30
+ <Text size="lg" color="muted" align="center" className="max-w-md">
31
+ Your app is ready. Sign in to get started or explore the API.
32
+ </Text>
33
+ <Flex justify="center" gap="md" className="pt-2">
34
+ {isAuthenticated ? (
35
+ <Button size="lg" asChild>
36
+ <Link to={Path.Dashboard}>Dashboard</Link>
37
+ </Button>
38
+ ) : (
39
+ <Button size="lg" asChild>
40
+ <Link to={Path.Login}>Sign In</Link>
41
+ </Button>
42
+ )}
43
+ <Button variant="outline" size="lg" asChild>
44
+ <a href={`${API_URL}/api/docs/`} target="_blank" rel="noopener noreferrer">
45
+ API Docs
46
+ </a>
47
+ </Button>
48
+ </Flex>
49
+ </Stack>
50
+
51
+ <Grid columns=\{{ base: 1, md: 3 }} gap={4} className="max-w-3xl mx-auto w-full">
52
+ <Card>
53
+ <CardHeader className="pb-2">
54
+ <Flex align="center" gap={2}>
55
+ <Terminal className="h-4 w-4 text-primary" />
56
+ <CardTitle className="text-sm">CLI</CardTitle>
57
+ </Flex>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <CardDescription>
61
+ Run <Text as="code" size="sm" className="font-mono">blacksmith dev</Text> to start the full stack.
62
+ </CardDescription>
63
+ </CardContent>
64
+ </Card>
65
+
66
+ <Card>
67
+ <CardHeader className="pb-2">
68
+ <Flex align="center" gap={2}>
69
+ <BookOpen className="h-4 w-4 text-primary" />
70
+ <CardTitle className="text-sm">API</CardTitle>
71
+ </Flex>
72
+ </CardHeader>
73
+ <CardContent>
74
+ <CardDescription>
75
+ <a href={`${API_URL}/api/docs/`} target="_blank" rel="noopener noreferrer" className="underline underline-offset-4 hover:text-foreground">
76
+ Swagger UI
77
+ </a>
78
+ {' / '}
79
+ <a href={`${API_URL}/admin/`} target="_blank" rel="noopener noreferrer" className="underline underline-offset-4 hover:text-foreground">
80
+ Django Admin
81
+ </a>
82
+ </CardDescription>
83
+ </CardContent>
84
+ </Card>
85
+
86
+ <Card>
87
+ <CardHeader className="pb-2">
88
+ <Flex align="center" gap={2}>
89
+ <Layout className="h-4 w-4 text-primary" />
90
+ <CardTitle className="text-sm">Frontend</CardTitle>
91
+ </Flex>
92
+ </CardHeader>
93
+ <CardContent>
94
+ <CardDescription>
95
+ React, TanStack Query, and Tailwind CSS with typed API hooks.
96
+ </CardDescription>
97
+ </CardContent>
98
+ </Card>
99
+ </Grid>
18
100
  </Stack>
19
101
  )
20
102
  }
@@ -11,6 +11,7 @@
11
11
  "moduleDetection": "force",
12
12
  "noEmit": true,
13
13
  "jsx": "react-jsx",
14
+ "types": ["vitest/globals"],
14
15
  "strict": true,
15
16
  "noUnusedLocals": true,
16
17
  "noUnusedParameters": true,
@@ -1,3 +1,4 @@
1
+ /// <reference types="vitest/config" />
1
2
  import { defineConfig } from 'vite'
2
3
  import react from '@vitejs/plugin-react'
3
4
  import { resolve } from 'path'
@@ -18,4 +19,11 @@ export default defineConfig({
18
19
  },
19
20
  },
20
21
  },
22
+ test: {
23
+ globals: true,
24
+ environment: 'jsdom',
25
+ setupFiles: ['./src/__tests__/setup.ts'],
26
+ include: ['src/**/*.spec.{ts,tsx}'],
27
+ css: true,
28
+ },
21
29
  })
@@ -0,0 +1,2 @@
1
+ export { use{{Names}}, useGet{{Name}} } from './use-{{kebabs}}-query'
2
+ export { useCreate{{Name}}, useUpdate{{Name}}, useDelete{{Name}} } from './use-{{kebab}}-mutations'
@@ -1,13 +1,14 @@
1
1
  /**
2
- * use{{Names}} Hook
2
+ * {{Name}} Query Hooks
3
3
  *
4
- * Wraps the generated list query with smart retry and error parsing.
4
+ * List and detail queries with smart retry and error parsing.
5
5
  * Generated by Blacksmith. You own this file — customize as needed.
6
6
  */
7
7
 
8
8
  import { useApiQuery } from '@/shared/hooks/use-api-query'
9
9
  import {
10
10
  {{snakes}}ListOptions,
11
+ {{snakes}}RetrieveOptions,
11
12
  } from '@/api/generated/@tanstack/react-query.gen'
12
13
 
13
14
  interface Use{{Names}}Params {
@@ -33,3 +34,10 @@ export function use{{Names}}(params: Use{{Names}}Params = {}) {
33
34
  }),
34
35
  })
35
36
  }
37
+
38
+ export function useGet{{Name}}(id: string) {
39
+ return useApiQuery({
40
+ ...{{snakes}}RetrieveOptions({ path: { id } }),
41
+ enabled: !!id,
42
+ })
43
+ }
@@ -21,7 +21,7 @@ export function useCreate{{Name}}() {
21
21
  })
22
22
  }
23
23
 
24
- export function useUpdate{{Name}}(id: number) {
24
+ export function useUpdate{{Name}}(id: string) {
25
25
  return useApiMutation({
26
26
  ...{{snakes}}UpdateMutation(),
27
27
  invalidateKeys: [
@@ -1,8 +1,9 @@
1
1
  from django.db import models
2
2
  from django.conf import settings
3
+ from utils.models import BaseModel
3
4
 
4
5
 
5
- class {{Name}}(models.Model):
6
+ class {{Name}}(BaseModel):
6
7
  """{{Name}} model."""
7
8
 
8
9
  title = models.CharField(max_length=255)
@@ -12,8 +13,6 @@ class {{Name}}(models.Model):
12
13
  on_delete=models.CASCADE,
13
14
  related_name='{{snakes}}',
14
15
  )
15
- created_at = models.DateTimeField(auto_now_add=True)
16
- updated_at = models.DateTimeField(auto_now=True)
17
16
 
18
17
  class Meta:
19
18
  ordering = ['-created_at']
@@ -10,7 +10,7 @@ import { Path } from '@/router/paths'
10
10
 
11
11
  interface {{Name}}CardProps {
12
12
  {{name}}: {
13
- id: number
13
+ id: string
14
14
  title: string
15
15
  description?: string
16
16
  created_at: string
@@ -9,7 +9,7 @@ import { {{Name}}Card } from './{{kebab}}-card'
9
9
 
10
10
  interface {{Name}}ListProps {
11
11
  {{names}}: Array<{
12
- id: number
12
+ id: string
13
13
  title: string
14
14
  description?: string
15
15
  created_at: string
@@ -1,6 +1,5 @@
1
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'
2
+ export { use{{Names}}, useCreate{{Name}}, useUpdate{{Name}}, useDelete{{Name}} } from '@/api/hooks/{{kebabs}}'
4
3
  export { {{Name}}Card } from './components/{{kebab}}-card'
5
4
  export { {{Name}}List } from './components/{{kebab}}-list'
6
5
  export { {{Name}}Form } from './components/{{kebab}}-form'
@@ -5,7 +5,7 @@
5
5
  * Generated by Blacksmith. You own this file — customize as needed.
6
6
  */
7
7
 
8
- import { use{{Names}} } from '../hooks/use-{{kebabs}}-query'
8
+ import { use{{Names}} } from '@/api/hooks/{{kebabs}}'
9
9
  import { {{Name}}List } from '../components/{{kebab}}-list'
10
10
  import { Alert, AlertDescription } from '@blacksmith-ui/react'
11
11
 
@@ -6,24 +6,16 @@
6
6
  */
7
7
 
8
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'
9
+ import { useGet{{Name}}, useDelete{{Name}} } from '@/api/hooks/{{kebabs}}'
11
10
  import { Alert, AlertDescription } from '@blacksmith-ui/react'
12
11
  import { Path } from '@/router/paths'
13
- import {
14
- {{snakes}}RetrieveOptions,
15
- } from '@/api/generated/@tanstack/react-query.gen'
16
12
 
17
13
  export default function {{Name}}DetailPage() {
18
14
  const { id } = useParams<{ id: string }>()
19
15
  const navigate = useNavigate()
20
16
  const delete{{Name}} = useDelete{{Name}}()
21
17
 
22
- const { data: {{name}}, isLoading, errorMessage } = useApiQuery({
23
- ...{{snakes}}RetrieveOptions({
24
- path: { id: Number(id) },
25
- }),
26
- })
18
+ const { data: {{name}}, isLoading, errorMessage } = useGet{{Name}}(id!)
27
19
 
28
20
  if (isLoading) {
29
21
  return (
@@ -49,7 +41,7 @@ export default function {{Name}}DetailPage() {
49
41
 
50
42
  const handleDelete = async () => {
51
43
  if (window.confirm('Are you sure you want to delete this {{name}}?')) {
52
- await delete{{Name}}.mutateAsync({ path: { id: Number(id) } })
44
+ await delete{{Name}}.mutateAsync({ path: { id: id! } })
53
45
  navigate(Path.{{Names}})
54
46
  }
55
47
  }
@@ -10,7 +10,7 @@ import { Path } from '@/router/paths'
10
10
 
11
11
  interface {{Name}}CardProps {
12
12
  {{name}}: {
13
- id: number
13
+ id: string
14
14
  title: string
15
15
  description?: string
16
16
  created_at: string
@@ -9,7 +9,7 @@ import { {{Name}}Card } from './{{kebab}}-card'
9
9
 
10
10
  interface {{Name}}ListProps {
11
11
  {{names}}: Array<{
12
- id: number
12
+ id: string
13
13
  title: string
14
14
  description?: string
15
15
  created_at: string
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Page-local hooks for {{Names}}
3
+ *
4
+ * This folder is for hooks that are specific to the {{Names}} page UI logic,
5
+ * such as form state, filtering, pagination, modals, and other page-level concerns.
6
+ *
7
+ * API hooks (queries and mutations) live in @/api/hooks/{{kebabs}}/ and should
8
+ * be imported from there.
9
+ */
@@ -1,6 +1,5 @@
1
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'
2
+ export { use{{Names}}, useCreate{{Name}}, useUpdate{{Name}}, useDelete{{Name}} } from '@/api/hooks/{{kebabs}}'
4
3
  export { {{Name}}Card } from './components/{{kebab}}-card'
5
4
  export { {{Name}}List } from './components/{{kebab}}-list'
6
5
  export { {{Name}}Form } from './components/{{kebab}}-form'
@@ -5,7 +5,7 @@
5
5
  * Generated by Blacksmith. You own this file — customize as needed.
6
6
  */
7
7
 
8
- import { use{{Names}} } from './hooks/use-{{kebabs}}-query'
8
+ import { use{{Names}} } from '@/api/hooks/{{kebabs}}'
9
9
  import { {{Name}}List } from './components/{{kebab}}-list'
10
10
  import { Alert, AlertDescription } from '@blacksmith-ui/react'
11
11
 
@@ -6,24 +6,16 @@
6
6
  */
7
7
 
8
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'
9
+ import { useGet{{Name}}, useDelete{{Name}} } from '@/api/hooks/{{kebabs}}'
11
10
  import { Alert, AlertDescription } from '@blacksmith-ui/react'
12
11
  import { Path } from '@/router/paths'
13
- import {
14
- {{snakes}}RetrieveOptions,
15
- } from '@/api/generated/@tanstack/react-query.gen'
16
12
 
17
13
  export default function {{Name}}DetailPage() {
18
14
  const { id } = useParams<{ id: string }>()
19
15
  const navigate = useNavigate()
20
16
  const delete{{Name}} = useDelete{{Name}}()
21
17
 
22
- const { data: {{name}}, isLoading, errorMessage } = useApiQuery({
23
- ...{{snakes}}RetrieveOptions({
24
- path: { id: Number(id) },
25
- }),
26
- })
18
+ const { data: {{name}}, isLoading, errorMessage } = useGet{{Name}}(id!)
27
19
 
28
20
  if (isLoading) {
29
21
  return (
@@ -49,7 +41,7 @@ export default function {{Name}}DetailPage() {
49
41
 
50
42
  const handleDelete = async () => {
51
43
  if (window.confirm('Are you sure you want to delete this {{name}}?')) {
52
- await delete{{Name}}.mutateAsync({ path: { id: Number(id) } })
44
+ await delete{{Name}}.mutateAsync({ path: { id: id! } })
53
45
  navigate(Path.{{Names}})
54
46
  }
55
47
  }
@@ -1,88 +0,0 @@
1
- import {
2
- Card,
3
- CardHeader,
4
- CardTitle,
5
- CardDescription,
6
- CardContent,
7
- Badge,
8
- Stack,
9
- Flex,
10
- Grid,
11
- Typography,
12
- Text,
13
- } from '@blacksmith-ui/react'
14
- import { Database, Layout, Shield, Code2, Zap, Rocket } from 'lucide-react'
15
-
16
- const features = [
17
- {
18
- icon: Database,
19
- title: 'Django REST API',
20
- description: 'Production-ready backend with JWT auth, serializers, and OpenAPI docs.',
21
- badge: 'Backend',
22
- },
23
- {
24
- icon: Layout,
25
- title: 'React + TypeScript',
26
- description: 'Vite-powered frontend with TanStack Query, React Router, and Tailwind CSS.',
27
- badge: 'Frontend',
28
- },
29
- {
30
- icon: Shield,
31
- title: 'Authentication',
32
- description: 'Complete auth flow with login, register, password reset, and role guards.',
33
- badge: 'Full Stack',
34
- },
35
- {
36
- icon: Code2,
37
- title: 'Type-Safe API',
38
- description: 'Auto-generated TypeScript client from your OpenAPI schema via blacksmith sync.',
39
- badge: 'DX',
40
- },
41
- {
42
- icon: Zap,
43
- title: 'Blacksmith UI',
44
- description: '80+ accessible components, form library, hooks, and dark mode theming.',
45
- badge: 'UI',
46
- },
47
- {
48
- icon: Rocket,
49
- title: 'Developer Experience',
50
- description: 'Hot reload, AI-ready CLAUDE.md, and one command to run the full stack.',
51
- badge: 'DX',
52
- },
53
- ]
54
-
55
- export function FeaturesGrid() {
56
- return (
57
- <Stack as="section" gap={6}>
58
- <Stack gap={2} align="center">
59
- <Typography variant="h2">What's Included</Typography>
60
- <Text color="muted">
61
- A batteries-included stack so you can focus on your product.
62
- </Text>
63
- </Stack>
64
- <Grid columns=\{{ base: 1, sm: 2, lg: 3 }} gap={4}>
65
- {features.map((feature) => (
66
- <Card key={feature.title} className="group transition-colors hover:border-primary/50">
67
- <CardHeader className="pb-3">
68
- <Flex align="start" justify="between">
69
- <Flex align="center" justify="center" className="h-10 w-10 rounded-lg bg-primary/10 transition-colors group-hover:bg-primary/20">
70
- <feature.icon className="h-5 w-5 text-primary" />
71
- </Flex>
72
- <Badge variant="outline" className="text-xs">
73
- {feature.badge}
74
- </Badge>
75
- </Flex>
76
- </CardHeader>
77
- <CardContent>
78
- <Stack gap={1.5}>
79
- <CardTitle className="text-base">{feature.title}</CardTitle>
80
- <CardDescription>{feature.description}</CardDescription>
81
- </Stack>
82
- </CardContent>
83
- </Card>
84
- ))}
85
- </Grid>
86
- </Stack>
87
- )
88
- }