amd-gaia 0.15.0__py3-none-any.whl → 0.15.2__py3-none-any.whl

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 (185) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/METADATA +222 -223
  2. amd_gaia-0.15.2.dist-info/RECORD +182 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/WHEEL +1 -1
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/entry_points.txt +1 -0
  5. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/licenses/LICENSE.md +20 -20
  6. gaia/__init__.py +29 -29
  7. gaia/agents/__init__.py +19 -19
  8. gaia/agents/base/__init__.py +9 -9
  9. gaia/agents/base/agent.py +2132 -2177
  10. gaia/agents/base/api_agent.py +119 -120
  11. gaia/agents/base/console.py +1967 -1841
  12. gaia/agents/base/errors.py +237 -237
  13. gaia/agents/base/mcp_agent.py +86 -86
  14. gaia/agents/base/tools.py +88 -83
  15. gaia/agents/blender/__init__.py +7 -0
  16. gaia/agents/blender/agent.py +553 -556
  17. gaia/agents/blender/agent_simple.py +133 -135
  18. gaia/agents/blender/app.py +211 -211
  19. gaia/agents/blender/app_simple.py +41 -41
  20. gaia/agents/blender/core/__init__.py +16 -16
  21. gaia/agents/blender/core/materials.py +506 -506
  22. gaia/agents/blender/core/objects.py +316 -316
  23. gaia/agents/blender/core/rendering.py +225 -225
  24. gaia/agents/blender/core/scene.py +220 -220
  25. gaia/agents/blender/core/view.py +146 -146
  26. gaia/agents/chat/__init__.py +9 -9
  27. gaia/agents/chat/agent.py +809 -835
  28. gaia/agents/chat/app.py +1065 -1058
  29. gaia/agents/chat/session.py +508 -508
  30. gaia/agents/chat/tools/__init__.py +15 -15
  31. gaia/agents/chat/tools/file_tools.py +96 -96
  32. gaia/agents/chat/tools/rag_tools.py +1744 -1729
  33. gaia/agents/chat/tools/shell_tools.py +437 -436
  34. gaia/agents/code/__init__.py +7 -7
  35. gaia/agents/code/agent.py +549 -549
  36. gaia/agents/code/cli.py +377 -0
  37. gaia/agents/code/models.py +135 -135
  38. gaia/agents/code/orchestration/__init__.py +24 -24
  39. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  40. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  41. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  42. gaia/agents/code/orchestration/factories/base.py +63 -63
  43. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  44. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  45. gaia/agents/code/orchestration/orchestrator.py +841 -841
  46. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  47. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  48. gaia/agents/code/orchestration/steps/base.py +188 -188
  49. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  50. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  51. gaia/agents/code/orchestration/steps/python.py +307 -307
  52. gaia/agents/code/orchestration/template_catalog.py +469 -469
  53. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  54. gaia/agents/code/orchestration/workflows/base.py +80 -80
  55. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  56. gaia/agents/code/orchestration/workflows/python.py +94 -94
  57. gaia/agents/code/prompts/__init__.py +11 -11
  58. gaia/agents/code/prompts/base_prompt.py +77 -77
  59. gaia/agents/code/prompts/code_patterns.py +2034 -2036
  60. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  61. gaia/agents/code/prompts/python_prompt.py +109 -109
  62. gaia/agents/code/schema_inference.py +365 -365
  63. gaia/agents/code/system_prompt.py +41 -41
  64. gaia/agents/code/tools/__init__.py +42 -42
  65. gaia/agents/code/tools/cli_tools.py +1138 -1138
  66. gaia/agents/code/tools/code_formatting.py +319 -319
  67. gaia/agents/code/tools/code_tools.py +769 -769
  68. gaia/agents/code/tools/error_fixing.py +1347 -1347
  69. gaia/agents/code/tools/external_tools.py +180 -180
  70. gaia/agents/code/tools/file_io.py +845 -845
  71. gaia/agents/code/tools/prisma_tools.py +190 -190
  72. gaia/agents/code/tools/project_management.py +1016 -1016
  73. gaia/agents/code/tools/testing.py +321 -321
  74. gaia/agents/code/tools/typescript_tools.py +122 -122
  75. gaia/agents/code/tools/validation_parsing.py +461 -461
  76. gaia/agents/code/tools/validation_tools.py +806 -806
  77. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  78. gaia/agents/code/validators/__init__.py +16 -16
  79. gaia/agents/code/validators/antipattern_checker.py +241 -241
  80. gaia/agents/code/validators/ast_analyzer.py +197 -197
  81. gaia/agents/code/validators/requirements_validator.py +145 -145
  82. gaia/agents/code/validators/syntax_validator.py +171 -171
  83. gaia/agents/docker/__init__.py +7 -7
  84. gaia/agents/docker/agent.py +643 -642
  85. gaia/agents/emr/__init__.py +8 -8
  86. gaia/agents/emr/agent.py +1504 -1506
  87. gaia/agents/emr/cli.py +1322 -1322
  88. gaia/agents/emr/constants.py +475 -475
  89. gaia/agents/emr/dashboard/__init__.py +4 -4
  90. gaia/agents/emr/dashboard/server.py +1972 -1974
  91. gaia/agents/jira/__init__.py +11 -11
  92. gaia/agents/jira/agent.py +894 -894
  93. gaia/agents/jira/jql_templates.py +299 -299
  94. gaia/agents/routing/__init__.py +7 -7
  95. gaia/agents/routing/agent.py +567 -570
  96. gaia/agents/routing/system_prompt.py +75 -75
  97. gaia/agents/summarize/__init__.py +11 -0
  98. gaia/agents/summarize/agent.py +885 -0
  99. gaia/agents/summarize/prompts.py +129 -0
  100. gaia/api/__init__.py +23 -23
  101. gaia/api/agent_registry.py +238 -238
  102. gaia/api/app.py +305 -305
  103. gaia/api/openai_server.py +575 -575
  104. gaia/api/schemas.py +186 -186
  105. gaia/api/sse_handler.py +373 -373
  106. gaia/apps/__init__.py +4 -4
  107. gaia/apps/llm/__init__.py +6 -6
  108. gaia/apps/llm/app.py +184 -169
  109. gaia/apps/summarize/app.py +116 -633
  110. gaia/apps/summarize/html_viewer.py +133 -133
  111. gaia/apps/summarize/pdf_formatter.py +284 -284
  112. gaia/audio/__init__.py +2 -2
  113. gaia/audio/audio_client.py +439 -439
  114. gaia/audio/audio_recorder.py +269 -269
  115. gaia/audio/kokoro_tts.py +599 -599
  116. gaia/audio/whisper_asr.py +432 -432
  117. gaia/chat/__init__.py +16 -16
  118. gaia/chat/app.py +428 -430
  119. gaia/chat/prompts.py +522 -522
  120. gaia/chat/sdk.py +1228 -1225
  121. gaia/cli.py +5659 -5632
  122. gaia/database/__init__.py +10 -10
  123. gaia/database/agent.py +176 -176
  124. gaia/database/mixin.py +290 -290
  125. gaia/database/testing.py +64 -64
  126. gaia/eval/batch_experiment.py +2332 -2332
  127. gaia/eval/claude.py +542 -542
  128. gaia/eval/config.py +37 -37
  129. gaia/eval/email_generator.py +512 -512
  130. gaia/eval/eval.py +3179 -3179
  131. gaia/eval/groundtruth.py +1130 -1130
  132. gaia/eval/transcript_generator.py +582 -582
  133. gaia/eval/webapp/README.md +167 -167
  134. gaia/eval/webapp/package-lock.json +875 -875
  135. gaia/eval/webapp/package.json +20 -20
  136. gaia/eval/webapp/public/app.js +3402 -3402
  137. gaia/eval/webapp/public/index.html +87 -87
  138. gaia/eval/webapp/public/styles.css +3661 -3661
  139. gaia/eval/webapp/server.js +415 -415
  140. gaia/eval/webapp/test-setup.js +72 -72
  141. gaia/installer/__init__.py +23 -0
  142. gaia/installer/init_command.py +1275 -0
  143. gaia/installer/lemonade_installer.py +619 -0
  144. gaia/llm/__init__.py +10 -2
  145. gaia/llm/base_client.py +60 -0
  146. gaia/llm/exceptions.py +12 -0
  147. gaia/llm/factory.py +70 -0
  148. gaia/llm/lemonade_client.py +3421 -3221
  149. gaia/llm/lemonade_manager.py +294 -294
  150. gaia/llm/providers/__init__.py +9 -0
  151. gaia/llm/providers/claude.py +108 -0
  152. gaia/llm/providers/lemonade.py +118 -0
  153. gaia/llm/providers/openai_provider.py +79 -0
  154. gaia/llm/vlm_client.py +382 -382
  155. gaia/logger.py +189 -189
  156. gaia/mcp/agent_mcp_server.py +245 -245
  157. gaia/mcp/blender_mcp_client.py +138 -138
  158. gaia/mcp/blender_mcp_server.py +648 -648
  159. gaia/mcp/context7_cache.py +332 -332
  160. gaia/mcp/external_services.py +518 -518
  161. gaia/mcp/mcp_bridge.py +811 -550
  162. gaia/mcp/servers/__init__.py +6 -6
  163. gaia/mcp/servers/docker_mcp.py +83 -83
  164. gaia/perf_analysis.py +361 -0
  165. gaia/rag/__init__.py +10 -10
  166. gaia/rag/app.py +293 -293
  167. gaia/rag/demo.py +304 -304
  168. gaia/rag/pdf_utils.py +235 -235
  169. gaia/rag/sdk.py +2194 -2194
  170. gaia/security.py +183 -163
  171. gaia/talk/app.py +287 -289
  172. gaia/talk/sdk.py +538 -538
  173. gaia/testing/__init__.py +87 -87
  174. gaia/testing/assertions.py +330 -330
  175. gaia/testing/fixtures.py +333 -333
  176. gaia/testing/mocks.py +493 -493
  177. gaia/util.py +46 -46
  178. gaia/utils/__init__.py +33 -33
  179. gaia/utils/file_watcher.py +675 -675
  180. gaia/utils/parsing.py +223 -223
  181. gaia/version.py +100 -100
  182. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  183. gaia/agents/code/app.py +0 -266
  184. gaia/llm/llm_client.py +0 -723
  185. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.2.dist-info}/top_level.txt +0 -0
@@ -1,2036 +1,2034 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """Code generation patterns for web applications.
4
-
5
- This module contains reusable code patterns for generating functional
6
- web application code. Patterns are framework-agnostic where possible,
7
- with framework-specific variants where needed.
8
-
9
- Patterns are stored as template strings that can be formatted with
10
- resource-specific context (model names, fields, etc.).
11
- """
12
-
13
- # ========== App-Wide Layout and Styling ==========
14
-
15
- APP_LAYOUT = """import type {{ Metadata }} from "next";
16
- import {{ Inter }} from "next/font/google";
17
- import "./globals.css";
18
-
19
- const inter = Inter({{ subsets: ["latin"] }});
20
-
21
- export const metadata: Metadata = {{
22
- title: "{app_title}",
23
- description: "{app_description}",
24
- }};
25
-
26
- export default function RootLayout({{
27
- children,
28
- }}: Readonly<{{
29
- children: React.ReactNode;
30
- }}>) {{
31
- return (
32
- <html lang="en" className="dark">
33
- <body className={{`${{inter.className}} antialiased`}}>
34
- <div className="fixed inset-0 bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 -z-10" />
35
- <div className="fixed inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-900/20 via-transparent to-transparent -z-10" />
36
- {{children}}
37
- </body>
38
- </html>
39
- );
40
- }}"""
41
-
42
- APP_GLOBALS_CSS = """@tailwind base;
43
- @tailwind components;
44
- @tailwind utilities;
45
-
46
- :root {{
47
- --background: #0f0f1a;
48
- --foreground: #e2e8f0;
49
- }}
50
-
51
- body {{
52
- color: var(--foreground);
53
- background: var(--background);
54
- min-height: 100vh;
55
- }}
56
-
57
- /* Custom scrollbar */
58
- ::-webkit-scrollbar {{
59
- width: 8px;
60
- }}
61
-
62
- ::-webkit-scrollbar-track {{
63
- background: rgba(30, 41, 59, 0.3);
64
- }}
65
-
66
- ::-webkit-scrollbar-thumb {{
67
- background: rgba(99, 102, 241, 0.5);
68
- border-radius: 4px;
69
- }}
70
-
71
- ::-webkit-scrollbar-thumb:hover {{
72
- background: rgba(99, 102, 241, 0.7);
73
- }}
74
-
75
- @layer base {{
76
- /* Dark mode color scheme */
77
- html {{
78
- color-scheme: dark;
79
- }}
80
-
81
- /* Better form defaults for dark theme */
82
- input[type="checkbox"] {{
83
- color-scheme: dark;
84
- }}
85
- }}
86
-
87
- @layer components {{
88
- /* Glass card effect */
89
- .glass-card {{
90
- @apply bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl;
91
- }}
92
-
93
- /* Button variants */
94
- .btn-primary {{
95
- @apply bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium
96
- hover:from-indigo-600 hover:to-purple-600 transition-all duration-300
97
- hover:shadow-lg hover:shadow-indigo-500/25 active:scale-95 disabled:opacity-50;
98
- }}
99
-
100
- .btn-secondary {{
101
- @apply bg-slate-700/50 text-slate-200 px-6 py-3 rounded-xl font-medium border border-slate-600/50
102
- hover:bg-slate-700 transition-all duration-300 active:scale-95;
103
- }}
104
-
105
- .btn-danger {{
106
- @apply bg-gradient-to-r from-red-500 to-rose-500 text-white px-6 py-3 rounded-xl font-medium
107
- hover:from-red-600 hover:to-rose-600 transition-all duration-300
108
- hover:shadow-lg hover:shadow-red-500/25 active:scale-95 disabled:opacity-50;
109
- }}
110
-
111
- /* Input styling */
112
- .input-field {{
113
- @apply w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl
114
- text-slate-100 placeholder-slate-500
115
- focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50
116
- transition-all duration-300;
117
- }}
118
-
119
- /* Modern checkbox */
120
- .checkbox-modern {{
121
- @apply appearance-none w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50
122
- checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500
123
- checked:border-transparent cursor-pointer transition-all duration-300
124
- hover:border-indigo-400 focus:ring-2 focus:ring-indigo-500/50;
125
- }}
126
-
127
- /* Page title with gradient */
128
- .page-title {{
129
- @apply text-4xl font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400
130
- bg-clip-text text-transparent;
131
- }}
132
-
133
- /* Back link styling */
134
- .link-back {{
135
- @apply inline-flex items-center gap-2 text-slate-400 hover:text-indigo-400
136
- transition-colors duration-300;
137
- }}
138
- }}
139
-
140
- @layer utilities {{
141
- .text-balance {{
142
- text-wrap: balance;
143
- }}
144
- }}
145
- """
146
-
147
- # ========== Landing Page Pattern ==========
148
-
149
- LANDING_PAGE_WITH_LINKS = """import Link from "next/link";
150
-
151
- export default function Home() {{
152
- return (
153
- <main className="min-h-screen">
154
- <div className="container mx-auto px-4 py-12 max-w-4xl">
155
- <h1 className="page-title mb-8">Welcome</h1>
156
-
157
- <div className="grid gap-6">
158
- <Link
159
- href="/{resource_plural}"
160
- className="glass-card p-6 block hover:border-indigo-500/50 transition-all duration-300 group"
161
- >
162
- <div className="flex items-center justify-between">
163
- <div>
164
- <h2 className="text-2xl font-semibold text-slate-100 mb-2 group-hover:text-indigo-400 transition-colors">{Resource}s</h2>
165
- <p className="text-slate-400">{link_description}</p>
166
- </div>
167
- <svg className="w-6 h-6 text-slate-500 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M9 5l7 7-7 7" />
169
- </svg>
170
- </div>
171
- </Link>
172
- </div>
173
- </div>
174
- </main>
175
- );
176
- }}
177
- """
178
-
179
- # ========== API Route Patterns (Next.js) ==========
180
-
181
- API_ROUTE_GET = """export async function GET() {{
182
- try {{
183
- const {resource_plural} = await prisma.{resource}.findMany({{
184
- orderBy: {{ id: 'desc' }},
185
- take: 50
186
- }});
187
- return NextResponse.json({resource_plural});
188
- }} catch (error) {{
189
- console.error('GET /{resource}s error:', error);
190
- return NextResponse.json(
191
- {{ error: 'Failed to fetch {resource}s' }},
192
- {{ status: 500 }}
193
- );
194
- }}
195
- }}"""
196
-
197
- API_ROUTE_GET_PAGINATED = """export async function GET(request: Request) {{
198
- try {{
199
- const {{ searchParams }} = new URL(request.url);
200
- const page = parseInt(searchParams.get('page') || '1');
201
- const limit = parseInt(searchParams.get('limit') || '10');
202
- const skip = (page - 1) * limit;
203
-
204
- const [{resource_plural}, total] = await Promise.all([
205
- prisma.{resource}.findMany({{
206
- skip,
207
- take: limit,
208
- orderBy: {{ id: 'desc' }}
209
- }}),
210
- prisma.{resource}.count()
211
- ]);
212
-
213
- return NextResponse.json({{
214
- {resource_plural},
215
- pagination: {{
216
- page,
217
- limit,
218
- total,
219
- pages: Math.ceil(total / limit)
220
- }}
221
- }});
222
- }} catch (error) {{
223
- console.error('GET /{resource}s error:', error);
224
- return NextResponse.json(
225
- {{ error: 'Failed to fetch {resource}s' }},
226
- {{ status: 500 }}
227
- );
228
- }}
229
- }}"""
230
-
231
- API_ROUTE_POST = """export async function POST(request: Request) {{
232
- try {{
233
- const body = await request.json();
234
-
235
- // Validate request body
236
- const validatedData = {Resource}Schema.parse(body);
237
-
238
- const {resource} = await prisma.{resource}.create({{
239
- data: validatedData
240
- }});
241
-
242
- return NextResponse.json({resource}, {{ status: 201 }});
243
- }} catch (error) {{
244
- if (error instanceof z.ZodError) {{
245
- return NextResponse.json(
246
- {{ error: 'Invalid request data', details: error.issues }},
247
- {{ status: 400 }}
248
- );
249
- }}
250
-
251
- console.error('POST /{resource}s error:', error);
252
- return NextResponse.json(
253
- {{ error: 'Failed to create {resource}' }},
254
- {{ status: 500 }}
255
- );
256
- }}
257
- }}"""
258
-
259
- API_ROUTE_DYNAMIC_GET = """export async function GET(
260
- request: Request,
261
- {{ params }}: {{ params: {{ id: string }} }}
262
- ) {{
263
- try {{
264
- const id = parseInt(params.id);
265
-
266
- const {resource} = await prisma.{resource}.findUnique({{
267
- where: {{ id }}
268
- }});
269
-
270
- if (!{resource}) {{
271
- return NextResponse.json(
272
- {{ error: '{Resource} not found' }},
273
- {{ status: 404 }}
274
- );
275
- }}
276
-
277
- return NextResponse.json({resource});
278
- }} catch (error) {{
279
- console.error('GET /{resource}/[id] error:', error);
280
- return NextResponse.json(
281
- {{ error: 'Failed to fetch {resource}' }},
282
- {{ status: 500 }}
283
- );
284
- }}
285
- }}"""
286
-
287
- API_ROUTE_DYNAMIC_PATCH = """export async function PATCH(
288
- request: Request,
289
- {{ params }}: {{ params: {{ id: string }} }}
290
- ) {{
291
- try {{
292
- const id = parseInt(params.id);
293
- const body = await request.json();
294
-
295
- const validatedData = {Resource}UpdateSchema.parse(body);
296
-
297
- const {resource} = await prisma.{resource}.update({{
298
- where: {{ id }},
299
- data: validatedData
300
- }});
301
-
302
- return NextResponse.json({resource});
303
- }} catch (error) {{
304
- if (error instanceof z.ZodError) {{
305
- return NextResponse.json(
306
- {{ error: 'Invalid update data', details: error.issues }},
307
- {{ status: 400 }}
308
- );
309
- }}
310
-
311
- console.error('PATCH /{resource}/[id] error:', error);
312
- return NextResponse.json(
313
- {{ error: 'Failed to update {resource}' }},
314
- {{ status: 500 }}
315
- );
316
- }}
317
- }}"""
318
-
319
- API_ROUTE_DYNAMIC_DELETE = """export async function DELETE(
320
- request: Request,
321
- {{ params }}: {{ params: {{ id: string }} }}
322
- ) {{
323
- try {{
324
- const id = parseInt(params.id);
325
-
326
- await prisma.{resource}.delete({{
327
- where: {{ id }}
328
- }});
329
-
330
- return NextResponse.json({{ success: true }});
331
- }} catch (error) {{
332
- console.error('DELETE /{resource}/[id] error:', error);
333
- return NextResponse.json(
334
- {{ error: 'Failed to delete {resource}' }},
335
- {{ status: 500 }}
336
- );
337
- }}
338
- }}"""
339
-
340
- # ========== Validation Schema Patterns ==========
341
-
342
-
343
- def generate_zod_schema(resource_name: str, fields: dict) -> str:
344
- """Generate Zod validation schema for a resource.
345
-
346
- Args:
347
- resource_name: Name of the resource (e.g., "todo", "user")
348
- fields: Dictionary of field names to types
349
-
350
- Returns:
351
- TypeScript code for Zod schema
352
- """
353
- schema_fields = []
354
- for field_name, field_type in fields.items():
355
- if field_name in ["id", "createdAt", "updatedAt"]:
356
- continue # Skip auto-generated fields
357
-
358
- zod_type = _map_type_to_zod(field_type)
359
- schema_fields.append(f" {field_name}: {zod_type}")
360
-
361
- resource_capitalized = resource_name.capitalize()
362
-
363
- return f"""const {resource_capitalized}Schema = z.object({{
364
- {','.join(schema_fields)}
365
- }});
366
-
367
- const {resource_capitalized}UpdateSchema = {resource_capitalized}Schema.partial();
368
-
369
- type {resource_capitalized} = z.infer<typeof {resource_capitalized}Schema>;"""
370
-
371
-
372
- def _map_type_to_zod(field_type: str) -> str:
373
- """Map field type to Zod validation type."""
374
- # Normalize to lowercase for consistent lookup
375
- normalized = field_type.lower()
376
-
377
- type_mapping = {
378
- "string": "z.string().min(1)",
379
- "text": "z.string()",
380
- "int": "z.number().int()",
381
- "number": "z.number().int()",
382
- "float": "z.number()",
383
- "boolean": "z.boolean()",
384
- "date": "z.coerce.date()",
385
- "datetime": "z.coerce.date()",
386
- "timestamp": "z.coerce.date()",
387
- "email": "z.string().email()",
388
- "url": "z.string().url()",
389
- }
390
- return type_mapping.get(normalized, "z.string()")
391
-
392
-
393
- # ========== React Component Patterns ==========
394
-
395
- SERVER_COMPONENT_LIST = """import {{ prisma }} from "@/lib/prisma";
396
- import Link from "next/link";
397
- // EXTRA COMPONENT NOTE: Import any previously generated components/helpers as needed.
398
- // import {{ AdditionalComponent }} from "@/components/AdditionalComponent";
399
-
400
- async function get{Resource}s() {{
401
- const {resource_plural} = await prisma.{resource}.findMany({{
402
- orderBy: {{ id: "desc" }},
403
- take: 50
404
- }});
405
- return {resource_plural};
406
- }}
407
-
408
- export default async function {Resource}sPage() {{
409
- const {resource_plural} = await get{Resource}s();
410
-
411
- return (
412
- <div className="min-h-screen">
413
- <div className="container mx-auto px-4 py-12 max-w-4xl">
414
- {{/* Header + Custom Components */}}
415
- <div className="mb-10 flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
416
- <div>
417
- <h1 className="text-4xl font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-2">
418
- {Resource}s
419
- </h1>
420
- <p className="text-slate-400">
421
- {{{resource_plural}.length === 0
422
- ? "No items yet. Create your first one!"
423
- : `${{({resource_plural} as any[]).filter(t => !(t as any).completed).length}} pending items`}}
424
- </p>
425
- </div>
426
-
427
- {{/* EXTRA COMPONENT NOTE:
428
- Check the plan for other generated components (timer, stats badge, etc.)
429
- and render them here via their imports. Example:
430
- <AdditionalComponent targetTimestamp={{...}} />
431
- Remove this placeholder when no extra component is needed. */}}
432
- {{/* <AdditionalComponent className="w-full md:w-60" /> */}}
433
- </div>
434
-
435
- {{/* Add Button */}}
436
- <div className="mb-8">
437
- <Link
438
- href="/{resource}s/new"
439
- className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300 hover:shadow-lg hover:shadow-indigo-500/25"
440
- >
441
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
442
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M12 4v16m8-8H4" />
443
- </svg>
444
- Add New {Resource}
445
- </Link>
446
- </div>
447
-
448
- {{/* List */}}
449
- <div className="bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl p-6">
450
- {{{resource_plural}.length === 0 ? (
451
- <div className="text-center py-16">
452
- <div className="w-20 h-20 mx-auto mb-6 rounded-full bg-slate-800/50 flex items-center justify-center">
453
- <svg className="w-10 h-10 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
454
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{1.5}} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
455
- </svg>
456
- </div>
457
- <h3 className="text-xl font-medium text-slate-300 mb-2">No {resource}s yet</h3>
458
- <p className="text-slate-500 mb-6">Create your first item to get started</p>
459
- <Link
460
- href="/{resource}s/new"
461
- className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300"
462
- >
463
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
464
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M12 4v16m8-8H4" />
465
- </svg>
466
- Create {Resource}
467
- </Link>
468
- </div>
469
- ) : (
470
- <div className="space-y-3">
471
- {{{resource_plural}.map((item) => (
472
- <Link
473
- key={{item.id}}
474
- href={{`/{resource}s/${{item.id}}`}}
475
- className="block p-5 rounded-xl bg-slate-800/30 border border-slate-700/30 hover:bg-slate-800/50 hover:border-indigo-500/30 transition-all duration-300"
476
- >
477
- <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
478
- <div className="flex-1">{field_display}</div>
479
- {{/* EXTRA COMPONENT NOTE:
480
- Check the plan for per-item components that were generated (countdown,
481
- status badge, etc.) and include them here. Example:
482
- <AdditionalComponent targetTimestamp={{item.missionTime}} />
483
- Remove this placeholder if no extra component is needed. */}}
484
- {{/* <AdditionalComponent {...item} className="w-full md:w-56" /> */}}
485
- </div>
486
- </Link>
487
- ))}}
488
- </div>
489
- )}}
490
- </div>
491
- </div>
492
- </div>
493
- );
494
- }}"""
495
-
496
- CLIENT_COMPONENT_FORM = """"use client";
497
-
498
- import {{ useState, useEffect }} from "react";
499
- import {{ useRouter }} from "next/navigation";
500
- import type {{ {Resource} }} from "@prisma/client";
501
-
502
- interface {Resource}FormProps {{
503
- initialData?: Partial<{Resource}>;
504
- mode?: "create" | "edit";
505
- }}
506
-
507
- export function {Resource}Form({{ initialData, mode = "create" }}: {Resource}FormProps) {{
508
- const router = useRouter();
509
- const [loading, setLoading] = useState(false);
510
- const [error, setError] = useState<string | null>(null);
511
-
512
- const [formData, setFormData] = useState({{
513
- {form_state_fields}
514
- }});
515
- const dateFields = {date_fields};
516
-
517
- const normalizePayload = (data: typeof formData) => {{
518
- if (dateFields.length === 0) {{
519
- return data;
520
- }}
521
-
522
- const normalized = {{ ...data }};
523
-
524
- dateFields.forEach((field) => {{
525
- const value = normalized[field as keyof typeof normalized];
526
- if (!value) {{
527
- return;
528
- }}
529
-
530
- const parsedValue = new Date(value as string | number | Date);
531
- if (!Number.isNaN(parsedValue.getTime())) {{
532
- (normalized as any)[field] = parsedValue.toISOString();
533
- }}
534
- }});
535
-
536
- return normalized;
537
- }};
538
-
539
- // Initialize form with initialData when in edit mode
540
- useEffect(() => {{
541
- if (initialData && mode === "edit") {{
542
- setFormData(prev => ({{
543
- ...prev,
544
- ...Object.fromEntries(
545
- Object.entries(initialData).filter(([key]) =>
546
- !["id", "createdAt", "updatedAt"].includes(key)
547
- )
548
- )
549
- }}));
550
- }}
551
- }}, [initialData, mode]);
552
-
553
- const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {{
554
- const {{ name, value, type }} = e.target;
555
- const checked = (e.target as HTMLInputElement).checked;
556
-
557
- setFormData(prev => ({{
558
- ...prev,
559
- [name]: type === "checkbox" ? checked : type === "number" ? parseFloat(value) : value
560
- }}));
561
- }};
562
-
563
- const handleSubmit = async (e: React.FormEvent) => {{
564
- e.preventDefault();
565
- setLoading(true);
566
- setError(null);
567
-
568
- try {{
569
- const url = mode === "create"
570
- ? "/api/{resource}s"
571
- : `/api/{resource}s/${{initialData?.id}}`;
572
-
573
- const method = mode === "create" ? "POST" : "PATCH";
574
- const payload = normalizePayload(formData);
575
-
576
- const response = await fetch(url, {{
577
- method,
578
- headers: {{ "Content-Type": "application/json" }},
579
- body: JSON.stringify(payload)
580
- }});
581
-
582
- if (!response.ok) {{
583
- const data = await response.json();
584
- throw new Error(data.error || "Operation failed");
585
- }}
586
-
587
- router.push("/{resource}s");
588
- router.refresh();
589
- }} catch (err) {{
590
- setError(err instanceof Error ? err.message : "An error occurred");
591
- }} finally {{
592
- setLoading(false);
593
- }}
594
- }};
595
-
596
- return (
597
- <form onSubmit={{handleSubmit}} className="space-y-6">
598
- {form_fields}
599
-
600
- {{error && (
601
- <div className="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-xl">
602
- {{error}}
603
- </div>
604
- )}}
605
-
606
- <div className="flex gap-4 pt-4">
607
- <button
608
- type="submit"
609
- disabled={{loading}}
610
- className="flex-1 bg-gradient-to-r from-indigo-500 to-purple-500 text-white py-3 px-6 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300 hover:shadow-lg hover:shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
611
- >
612
- {{loading ? "Saving..." : mode === "create" ? "Create {Resource}" : "Save Changes"}}
613
- </button>
614
- <button
615
- type="button"
616
- onClick={{() => router.back()}}
617
- className="px-6 py-3 bg-slate-700/50 text-slate-200 rounded-xl font-medium border border-slate-600/50 hover:bg-slate-700 transition-all duration-300"
618
- >
619
- Cancel
620
- </button>
621
- </div>
622
- </form>
623
- );
624
- }}"""
625
-
626
- CLIENT_COMPONENT_TIMER = """"use client";
627
-
628
- import {{ useEffect, useMemo, useState }} from "react";
629
-
630
- interface {{ComponentName}}Props {{
631
- targetTimestamp?: string; // ISO 8601 string that marks when the countdown ends
632
- durationSeconds?: number; // Fallback duration (seconds) when no timestamp is provided
633
- className?: string;
634
- }}
635
-
636
- const MS_IN_SECOND = 1000;
637
- const MS_IN_MINUTE = 60 * MS_IN_SECOND;
638
- const MS_IN_HOUR = 60 * MS_IN_MINUTE;
639
- const MS_IN_DAY = 24 * MS_IN_HOUR;
640
-
641
- export function {{ComponentName}}({{
642
- targetTimestamp,
643
- durationSeconds = 0,
644
- className = "",
645
- }}: {{ComponentName}}Props) {{
646
- const deadlineMs = useMemo(() => {{
647
- if (targetTimestamp) {{
648
- const parsed = Date.parse(targetTimestamp);
649
- return Number.isNaN(parsed) ? null : parsed;
650
- }}
651
- if (durationSeconds > 0) {{
652
- return Date.now() + durationSeconds * MS_IN_SECOND;
653
- }}
654
- return null;
655
- }}, [targetTimestamp, durationSeconds]);
656
-
657
- const [timeLeftMs, setTimeLeftMs] = useState(() => {{
658
- if (!deadlineMs) return 0;
659
- return Math.max(deadlineMs - Date.now(), 0);
660
- }});
661
-
662
- useEffect(() => {{
663
- if (!deadlineMs) {{
664
- setTimeLeftMs(0);
665
- return;
666
- }}
667
-
668
- const update = () => {{
669
- setTimeLeftMs(Math.max(deadlineMs - Date.now(), 0));
670
- }};
671
-
672
- update();
673
-
674
- const intervalId = window.setInterval(() => {{
675
- update();
676
- if (deadlineMs <= Date.now()) {{
677
- window.clearInterval(intervalId);
678
- }}
679
- }}, 1000);
680
-
681
- return () => window.clearInterval(intervalId);
682
- }}, [deadlineMs]);
683
-
684
- const isExpired = timeLeftMs <= 0;
685
-
686
- // TIMER_NOTE: derive whichever granularity the feature demands (days, hours,
687
- // minutes, seconds, milliseconds, etc.). Remove unused helpers so the final
688
- // output matches the spec exactly.
689
- const days = Math.floor(timeLeftMs / MS_IN_DAY);
690
- const hours = Math.floor((timeLeftMs % MS_IN_DAY) / MS_IN_HOUR);
691
- const minutes = Math.floor((timeLeftMs % MS_IN_HOUR) / MS_IN_MINUTE);
692
- const seconds = Math.floor((timeLeftMs % MS_IN_MINUTE) / MS_IN_SECOND);
693
-
694
- return (
695
- <section
696
- className={{`glass-card p-6 space-y-4 ${{className}}`.trim()}}
697
- data-countdown-target={{targetTimestamp || ""}}
698
- >
699
- {{/* TIMER_NOTE: swap this placeholder layout for the requested display.
700
- Emit only the units the user cares about (e.g., just minutes/seconds,
701
- or a full days→hours→minutes breakdown). */}}
702
- <div className="font-mono text-4xl text-slate-100">
703
- {{seconds}}s
704
- </div>
705
-
706
- {{isExpired && (
707
- <p className="text-sm text-slate-400">
708
- {{/* TIMER_NOTE: replace this placeholder with the exact completion
709
- copy or follow-up action the prompt describes. */}}
710
- Countdown complete.
711
- </p>
712
- )}}
713
- </section>
714
- );
715
- }}"""
716
-
717
-
718
- CLIENT_COMPONENT_NEW_PAGE = """"use client";
719
-
720
- import {{ {Resource}Form }} from "@/components/{Resource}Form";
721
- import Link from "next/link";
722
-
723
- export default function New{Resource}Page() {{
724
- return (
725
- <div className="min-h-screen">
726
- <div className="container mx-auto px-4 py-12 max-w-2xl">
727
- <div className="mb-8">
728
- <Link
729
- href="/{resource}s"
730
- className="link-back group"
731
- >
732
- <svg className="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
733
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M15 19l-7-7 7-7" />
734
- </svg>
735
- Back to {Resource}s
736
- </Link>
737
- </div>
738
-
739
- <div className="glass-card p-8">
740
- <h1 className="page-title mb-8">
741
- Create New {Resource}
742
- </h1>
743
-
744
- <{Resource}Form mode="create" />
745
- </div>
746
- </div>
747
- </div>
748
- );
749
- }}"""
750
-
751
- SERVER_COMPONENT_DETAIL = """"use client";
752
-
753
- import {{ useRouter }} from "next/navigation";
754
- import {{ useState, useEffect }} from "react";
755
- import Link from "next/link";
756
-
757
- interface {Resource}Data {{
758
- id: number;
759
- {interface_fields}
760
- createdAt: string;
761
- updatedAt: string;
762
- }}
763
-
764
- export default function {Resource}EditPage({{
765
- params
766
- }}: {{
767
- params: {{ id: string }}
768
- }}) {{
769
- const router = useRouter();
770
- const id = parseInt(params.id);
771
- const [loading, setLoading] = useState(true);
772
- const [saving, setSaving] = useState(false);
773
- const [deleting, setDeleting] = useState(false);
774
- const [error, setError] = useState<string | null>(null);
775
- const [{resource}, set{Resource}] = useState<{Resource}Data | null>(null);
776
-
777
- // Form state - populated from API
778
- {form_state}
779
-
780
- // Fetch data on mount
781
- useEffect(() => {{
782
- async function fetchData() {{
783
- try {{
784
- const response = await fetch(`/api/{resource}s/${{id}}`);
785
- if (!response.ok) {{
786
- if (response.status === 404) {{
787
- router.push("/{resource}s");
788
- return;
789
- }}
790
- throw new Error("Failed to fetch {resource}");
791
- }}
792
- const data = await response.json();
793
- set{Resource}(data);
794
- // Populate form fields
795
- {populate_fields}
796
- setLoading(false);
797
- }} catch (err) {{
798
- setError(err instanceof Error ? err.message : "An error occurred");
799
- setLoading(false);
800
- }}
801
- }}
802
- fetchData();
803
- }}, [id, router]);
804
-
805
- const handleSave = async (e: React.FormEvent) => {{
806
- e.preventDefault();
807
- setSaving(true);
808
- setError(null);
809
-
810
- try {{
811
- const response = await fetch(`/api/{resource}s/${{id}}`, {{
812
- method: "PATCH",
813
- headers: {{ "Content-Type": "application/json" }},
814
- body: JSON.stringify({{
815
- {save_body}
816
- }}),
817
- }});
818
-
819
- if (!response.ok) {{
820
- const data = await response.json();
821
- throw new Error(data.error || "Failed to update {resource}");
822
- }}
823
-
824
- router.push("/{resource}s");
825
- router.refresh();
826
- }} catch (err) {{
827
- setError(err instanceof Error ? err.message : "An error occurred");
828
- setSaving(false);
829
- }}
830
- }};
831
-
832
- const handleDelete = async () => {{
833
- if (!confirm("Are you sure you want to delete this {resource}?")) {{
834
- return;
835
- }}
836
-
837
- setDeleting(true);
838
- setError(null);
839
-
840
- try {{
841
- const response = await fetch(`/api/{resource}s/${{id}}`, {{
842
- method: "DELETE"
843
- }});
844
-
845
- if (!response.ok) {{
846
- throw new Error("Failed to delete {resource}");
847
- }}
848
-
849
- router.push("/{resource}s");
850
- router.refresh();
851
- }} catch (err) {{
852
- setError(err instanceof Error ? err.message : "An error occurred");
853
- setDeleting(false);
854
- }}
855
- }};
856
-
857
- if (loading) {{
858
- return (
859
- <div className="min-h-screen flex items-center justify-center">
860
- <div className="text-slate-400">Loading...</div>
861
- </div>
862
- );
863
- }}
864
-
865
- if (!{resource}) {{
866
- return (
867
- <div className="min-h-screen flex items-center justify-center">
868
- <div className="text-slate-400">{Resource} not found</div>
869
- </div>
870
- );
871
- }}
872
-
873
- return (
874
- <div className="min-h-screen">
875
- <div className="container mx-auto px-4 py-12 max-w-2xl">
876
- <div className="mb-8">
877
- <Link
878
- href="/{resource}s"
879
- className="link-back group"
880
- >
881
- <svg className="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
882
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M15 19l-7-7 7-7" />
883
- </svg>
884
- Back to {Resource}s
885
- </Link>
886
- </div>
887
-
888
- <div className="glass-card p-8">
889
- <h1 className="page-title mb-8">
890
- Edit {Resource}
891
- </h1>
892
-
893
- {{error && (
894
- <div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400">
895
- {{error}}
896
- </div>
897
- )}}
898
-
899
- <form onSubmit={{handleSave}}>
900
- {form_fields}
901
-
902
- <div className="flex gap-4 mt-8">
903
- <button
904
- type="submit"
905
- disabled={{saving}}
906
- className="btn-primary flex-1"
907
- >
908
- {{saving ? "Saving..." : "Save Changes"}}
909
- </button>
910
- <button
911
- type="button"
912
- onClick={{handleDelete}}
913
- disabled={{deleting}}
914
- className="btn-danger"
915
- >
916
- {{deleting ? "Deleting..." : "Delete"}}
917
- </button>
918
- </div>
919
- </form>
920
-
921
- <div className="mt-8 pt-6 border-t border-slate-700/50 text-sm text-slate-500">
922
- <p><strong className="text-slate-400">Created:</strong> {{new Date({resource}.createdAt).toLocaleString()}}</p>
923
- <p className="mt-1"><strong className="text-slate-400">Updated:</strong> {{new Date({resource}.updatedAt).toLocaleString()}}</p>
924
- </div>
925
- </div>
926
- </div>
927
- </div>
928
- );
929
- }}"""
930
-
931
- CLIENT_COMPONENT_ACTIONS = """"use client";
932
-
933
- import {{ useRouter }} from "next/navigation";
934
- import {{ useState }} from "react";
935
- import {{ {Resource}Form }} from "@/components/{Resource}Form";
936
- import type {{ {Resource} }} from "@prisma/client";
937
-
938
- interface {Resource}ActionsProps {{
939
- {resource}Id: number;
940
- {resource}Data?: {Resource};
941
- }}
942
-
943
- export function {Resource}Actions({{ {resource}Id, {resource}Data }}: {Resource}ActionsProps) {{
944
- const router = useRouter();
945
- const [isEditing, setIsEditing] = useState(false);
946
- const [deleting, setDeleting] = useState(false);
947
- const [error, setError] = useState<string | null>(null);
948
-
949
- const handleDelete = async () => {{
950
- if (!confirm("Are you sure you want to delete this {resource}?")) {{
951
- return;
952
- }}
953
-
954
- setDeleting(true);
955
- setError(null);
956
-
957
- try {{
958
- const response = await fetch(`/api/{resource}s/${{{resource}Id}}`, {{
959
- method: "DELETE"
960
- }});
961
-
962
- if (!response.ok) {{
963
- throw new Error("Failed to delete {resource}");
964
- }}
965
-
966
- router.push("/{resource}s");
967
- router.refresh();
968
- }} catch (err) {{
969
- setError(err instanceof Error ? err.message : "An error occurred");
970
- setDeleting(false);
971
- }}
972
- }};
973
-
974
- if (isEditing && {resource}Data) {{
975
- return (
976
- <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
977
- <div className="bg-slate-800 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
978
- <div className="p-6 border-b border-slate-700">
979
- <div className="flex justify-between items-center">
980
- <h2 className="text-xl font-bold text-slate-100">Edit {Resource}</h2>
981
- <button
982
- onClick={{() => setIsEditing(false)}}
983
- className="text-slate-400 hover:text-slate-200 transition-colors"
984
- >
985
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
986
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M6 18L18 6M6 6l12 12" />
987
- </svg>
988
- </button>
989
- </div>
990
- </div>
991
- <div className="p-6">
992
- <{Resource}Form initialData={{{resource}Data}} mode="edit" />
993
- </div>
994
- </div>
995
- </div>
996
- );
997
- }}
998
-
999
- return (
1000
- <div className="flex items-center gap-3">
1001
- {{error && (
1002
- <div className="absolute top-full right-0 mt-2 bg-red-500/10 border border-red-500/20 text-red-400 p-3 rounded-xl text-sm whitespace-nowrap">
1003
- {{error}}
1004
- </div>
1005
- )}}
1006
- <button
1007
- onClick={{() => setIsEditing(true)}}
1008
- className="inline-flex items-center gap-2 bg-slate-700/50 text-slate-200 px-4 py-2 rounded-xl font-medium border border-slate-600/50 hover:bg-slate-700 transition-all duration-300"
1009
- >
1010
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1011
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
1012
- </svg>
1013
- Edit
1014
- </button>
1015
- <button
1016
- onClick={{handleDelete}}
1017
- disabled={{deleting}}
1018
- className="inline-flex items-center gap-2 bg-gradient-to-r from-red-500 to-rose-500 text-white px-4 py-2 rounded-xl font-medium hover:from-red-600 hover:to-rose-600 transition-all duration-300 hover:shadow-lg hover:shadow-red-500/25 disabled:opacity-50"
1019
- >
1020
- <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1021
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
1022
- </svg>
1023
- {{deleting ? "Deleting..." : "Delete"}}
1024
- </button>
1025
- </div>
1026
- );
1027
- }}"""
1028
-
1029
- CLIENT_COMPONENT_DETAIL_PAGE = """"use client";
1030
-
1031
- import {{ useState, useEffect }} from "react";
1032
- import {{ useRouter }} from "next/navigation";
1033
- import {{ {Resource}Form }} from "@/components/{Resource}Form";
1034
- import Link from "next/link";
1035
-
1036
- interface {Resource} {{
1037
- id: number;
1038
- {type_fields}
1039
- createdAt: Date;
1040
- updatedAt: Date;
1041
- }}
1042
-
1043
- export default function {Resource}DetailPage({{ params }}: {{ params: {{ id: string }} }}) {{
1044
- const router = useRouter();
1045
- const [{resource}, set{Resource}] = useState<{Resource} | null>(null);
1046
- const [error, setError] = useState<string | null>(null);
1047
- const [loading, setLoading] = useState(true);
1048
- const [deleting, setDeleting] = useState(false);
1049
-
1050
- useEffect(() => {{
1051
- fetch{Resource}();
1052
- }}, [params.id]);
1053
-
1054
- const fetch{Resource} = async () => {{
1055
- try {{
1056
- const response = await fetch(`/api/{resource}s/${{params.id}}`);
1057
- if (!response.ok) {{
1058
- throw new Error("Failed to fetch {resource}");
1059
- }}
1060
- const data = await response.json();
1061
- set{Resource}(data);
1062
- }} catch (err) {{
1063
- setError(err instanceof Error ? err.message : "An error occurred");
1064
- }} finally {{
1065
- setLoading(false);
1066
- }}
1067
- }};
1068
-
1069
- const handleDelete = async () => {{
1070
- if (!confirm("Are you sure you want to delete this {resource}?")) return;
1071
-
1072
- setDeleting(true);
1073
- try {{
1074
- const response = await fetch(`/api/{resource}s/${{params.id}}`, {{
1075
- method: "DELETE"
1076
- }});
1077
-
1078
- if (!response.ok) {{
1079
- throw new Error("Failed to delete {resource}");
1080
- }}
1081
-
1082
- router.push("/{resource}s");
1083
- router.refresh();
1084
- }} catch (err) {{
1085
- setError(err instanceof Error ? err.message : "An error occurred");
1086
- setDeleting(false);
1087
- }}
1088
- }};
1089
-
1090
- if (loading) {{
1091
- return (
1092
- <div className="container mx-auto p-8 max-w-2xl">
1093
- <div className="text-center py-12">Loading...</div>
1094
- </div>
1095
- );
1096
- }}
1097
-
1098
- if (error || !{resource}) {{
1099
- return (
1100
- <div className="container mx-auto p-8 max-w-2xl">
1101
- <div className="text-center py-12">
1102
- <p className="text-red-500 mb-4">{{error || "{Resource} not found"}}</p>
1103
- <Link href="/{resource}s" className="text-blue-500 hover:underline">
1104
- Back to {Resource}s
1105
- </Link>
1106
- </div>
1107
- </div>
1108
- );
1109
- }}
1110
-
1111
- return (
1112
- <div className="container mx-auto p-8 max-w-2xl">
1113
- <div className="mb-6">
1114
- <Link href="/{resource}s" className="text-blue-500 hover:underline">
1115
- ← Back to {Resource}s
1116
- </Link>
1117
- </div>
1118
-
1119
- <h1 className="text-3xl font-bold mb-6">Edit {Resource}</h1>
1120
-
1121
- <{Resource}Form initialData={{{resource}}} mode="edit" />
1122
-
1123
- <div className="mt-6 pt-6 border-t border-gray-200">
1124
- <button
1125
- onClick={{handleDelete}}
1126
- disabled={{deleting}}
1127
- className="bg-red-500 text-white px-6 py-2 rounded-md hover:bg-red-600 disabled:opacity-50"
1128
- >
1129
- {{deleting ? "Deleting..." : "Delete {Resource}"}}
1130
- </button>
1131
- </div>
1132
-
1133
- <div className="mt-6 bg-gray-50 rounded-lg p-4 text-sm text-gray-600">
1134
- <p><strong>Created:</strong> {{new Date({resource}.createdAt).toLocaleString()}}</p>
1135
- <p><strong>Updated:</strong> {{new Date({resource}.updatedAt).toLocaleString()}}</p>
1136
- </div>
1137
- </div>
1138
- );
1139
- }}"""
1140
-
1141
-
1142
- def generate_form_field(field_name: str, field_type: str) -> str:
1143
- """Generate a form field based on type with modern styling."""
1144
- input_type = {
1145
- "string": "text",
1146
- "text": "textarea",
1147
- "number": "number",
1148
- "email": "email",
1149
- "url": "url",
1150
- "boolean": "checkbox",
1151
- "date": "date",
1152
- }.get(field_type.lower(), "text")
1153
-
1154
- label = field_name.replace("_", " ").title()
1155
-
1156
- if input_type == "textarea":
1157
- return f""" <div>
1158
- <label htmlFor="{field_name}" className="block text-sm font-medium text-slate-300 mb-2">
1159
- {label}
1160
- </label>
1161
- <textarea
1162
- id="{field_name}"
1163
- name="{field_name}"
1164
- value={{formData.{field_name}}}
1165
- onChange={{handleChange}}
1166
- rows={{4}}
1167
- className="w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all duration-300"
1168
- required
1169
- />
1170
- </div>"""
1171
- elif input_type == "checkbox":
1172
- return f""" <div className="flex items-center gap-3 p-4 bg-slate-900/30 rounded-xl border border-slate-700/30">
1173
- <input
1174
- type="checkbox"
1175
- id="{field_name}"
1176
- name="{field_name}"
1177
- checked={{formData.{field_name}}}
1178
- onChange={{handleChange}}
1179
- className="w-5 h-5 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent focus:ring-2 focus:ring-indigo-500/50 cursor-pointer transition-all duration-300"
1180
- />
1181
- <label htmlFor="{field_name}" className="text-slate-300 cursor-pointer">
1182
- {label}
1183
- </label>
1184
- </div>"""
1185
- else:
1186
- return f""" <div>
1187
- <label htmlFor="{field_name}" className="block text-sm font-medium text-slate-300 mb-2">
1188
- {label}
1189
- </label>
1190
- <input
1191
- type="{input_type}"
1192
- id="{field_name}"
1193
- name="{field_name}"
1194
- value={{formData.{field_name}}}
1195
- onChange={{handleChange}}
1196
- className="w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all duration-300"
1197
- required
1198
- />
1199
- </div>"""
1200
-
1201
-
1202
- # ========== Import Generation ==========
1203
-
1204
-
1205
- def generate_api_imports(_operations: list, uses_validation: bool = True) -> str:
1206
- """Generate appropriate imports for API routes."""
1207
- imports = [
1208
- 'import { NextResponse } from "next/server";',
1209
- 'import { prisma } from "@/lib/prisma";',
1210
- ]
1211
-
1212
- if uses_validation:
1213
- imports.append('import { z } from "zod";')
1214
-
1215
- return "\n".join(imports)
1216
-
1217
-
1218
- def generate_component_imports(component_type: str, uses_data: bool = False) -> str:
1219
- """Generate appropriate imports for React components."""
1220
- imports = []
1221
-
1222
- if component_type == "client":
1223
- imports.extend(
1224
- [
1225
- '"use client";',
1226
- "",
1227
- 'import { useState } from "react";',
1228
- 'import { useRouter } from "next/navigation";',
1229
- ]
1230
- )
1231
- elif uses_data:
1232
- imports.append('import { prisma } from "@/lib/prisma";')
1233
-
1234
- imports.append('import Link from "next/link";')
1235
-
1236
- return "\n".join(imports)
1237
-
1238
-
1239
- # ========== Helper Functions ==========
1240
-
1241
-
1242
- def pluralize(word: str) -> str:
1243
- """Simple pluralization (can be enhanced)."""
1244
- if word.endswith("y"):
1245
- return word[:-1] + "ies"
1246
- elif word.endswith(("s", "x", "z", "ch", "sh")):
1247
- return word + "es"
1248
- else:
1249
- return word + "s"
1250
-
1251
-
1252
- def generate_field_display(fields: dict, max_fields: int = 3) -> str:
1253
- """Generate JSX for displaying resource fields with type-aware rendering.
1254
-
1255
- Boolean fields render as checkboxes with strikethrough styling for completed items.
1256
- String fields render as text with proper hierarchy. Uses modern dark theme styling.
1257
-
1258
- Args:
1259
- fields: Dictionary mapping field names to their types (e.g., {"title": "string", "completed": "boolean"})
1260
- max_fields: Maximum number of fields to display (default: 3)
1261
-
1262
- Returns:
1263
- JSX string for field display
1264
- """
1265
- display_fields = []
1266
- title_field = None
1267
- boolean_field = None
1268
-
1269
- # Find primary title field and boolean field
1270
- for field_name, field_type in fields.items():
1271
- if field_name.lower() in {"id", "createdat", "updatedat"}:
1272
- continue
1273
- if field_name.lower() in {"title", "name"} and not title_field:
1274
- title_field = field_name
1275
- if field_type.lower() == "boolean" and not boolean_field:
1276
- boolean_field = field_name
1277
-
1278
- # Generate checkbox + title combo for boolean fields (e.g., completed todo)
1279
- if boolean_field and title_field:
1280
- # Render checkbox with title that has strikethrough when boolean is true
1281
- display_fields.append(
1282
- f"""<div className="flex items-center gap-4">
1283
- <div className="relative">
1284
- <input
1285
- type="checkbox"
1286
- checked={{item.{boolean_field}}}
1287
- readOnly
1288
- className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-pointer transition-all duration-300"
1289
- />
1290
- {{item.{boolean_field} && (
1291
- <svg className="absolute inset-0 w-6 h-6 text-white pointer-events-none p-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1292
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{3}} d="M5 13l4 4L19 7" />
1293
- </svg>
1294
- )}}
1295
- </div>
1296
- <h3 className={{`font-semibold text-lg ${{item.{boolean_field} ? "line-through text-slate-500" : "text-slate-100"}}`}}>
1297
- {{item.{title_field}}}
1298
- </h3>
1299
- </div>"""
1300
- )
1301
- elif title_field:
1302
- # Just render title without checkbox
1303
- display_fields.append(
1304
- f'<h3 className="font-semibold text-lg text-slate-100">{{item.{title_field}}}</h3>'
1305
- )
1306
-
1307
- # Add remaining non-boolean, non-title fields as secondary text
1308
- for field_name, field_type in list(fields.items())[:max_fields]:
1309
- if field_name.lower() in {"id", "createdat", "updatedat"}:
1310
- continue
1311
- if field_name == title_field or field_name == boolean_field:
1312
- continue
1313
- if field_type.lower() != "boolean":
1314
- display_fields.append(
1315
- f'<p className="text-slate-400 text-sm mt-1">{{item.{field_name}}}</p>'
1316
- )
1317
-
1318
- return (
1319
- "\n ".join(display_fields)
1320
- if display_fields
1321
- else '<p className="text-slate-400">{{item.id}}</p>'
1322
- )
1323
-
1324
-
1325
- def generate_new_page(resource_name: str) -> str:
1326
- """Generate a 'new' page component that uses the form component.
1327
-
1328
- Args:
1329
- resource_name: Name of the resource (e.g., "todo", "product")
1330
-
1331
- Returns:
1332
- Complete TypeScript/React page component code
1333
- """
1334
- resource = resource_name.lower()
1335
- Resource = resource_name.capitalize()
1336
-
1337
- return CLIENT_COMPONENT_NEW_PAGE.format(resource=resource, Resource=Resource)
1338
-
1339
-
1340
- def generate_detail_page(resource_name: str, fields: dict) -> str:
1341
- """Generate an edit page with pre-populated form fields.
1342
-
1343
- Args:
1344
- resource_name: Name of the resource (e.g., "todo", "product")
1345
- fields: Dictionary of field names to types
1346
-
1347
- Returns:
1348
- Complete TypeScript/React page component code
1349
- """
1350
- resource = resource_name.lower()
1351
- Resource = resource_name.capitalize()
1352
-
1353
- # Generate TypeScript interface fields
1354
- interface_lines = []
1355
- form_state_lines = []
1356
- populate_lines = []
1357
- save_body_lines = []
1358
- form_field_lines = []
1359
-
1360
- for field_name, field_type in fields.items():
1361
- if field_name.lower() in {"id", "createdat", "updatedat"}:
1362
- continue
1363
-
1364
- # TypeScript types
1365
- ts_type = _get_typescript_type(field_type)
1366
- interface_lines.append(f" {field_name}: {ts_type};")
1367
-
1368
- # useState declarations
1369
- default_val = _get_default_value(field_type)
1370
- form_state_lines.append(
1371
- f" const [{field_name}, set{field_name.capitalize()}] = useState<{ts_type}>({default_val});"
1372
- )
1373
-
1374
- # Populate from API response
1375
- populate_lines.append(
1376
- f" set{field_name.capitalize()}(data.{field_name});"
1377
- )
1378
-
1379
- # Save body
1380
- save_body_lines.append(f" {field_name},")
1381
-
1382
- # Form field JSX
1383
- label = field_name.replace("_", " ").title()
1384
- form_field_lines.append(
1385
- _generate_edit_form_field(field_name, field_type, label)
1386
- )
1387
-
1388
- return SERVER_COMPONENT_DETAIL.format(
1389
- resource=resource,
1390
- Resource=Resource,
1391
- interface_fields="\n".join(interface_lines),
1392
- form_state="\n".join(form_state_lines),
1393
- populate_fields="\n".join(populate_lines),
1394
- save_body="\n".join(save_body_lines),
1395
- form_fields="\n\n".join(form_field_lines),
1396
- )
1397
-
1398
-
1399
- def _get_typescript_type(field_type: str) -> str:
1400
- """Convert field type to TypeScript type."""
1401
- type_lower = field_type.lower()
1402
- if type_lower == "boolean":
1403
- return "boolean"
1404
- if type_lower in ["number", "int", "integer", "float"]:
1405
- return "number"
1406
- return "string"
1407
-
1408
-
1409
- def _get_default_value(field_type: str) -> str:
1410
- """Get default value for useState based on field type."""
1411
- type_lower = field_type.lower()
1412
- if type_lower == "boolean":
1413
- return "false"
1414
- if type_lower in ["number", "int", "integer", "float"]:
1415
- return "0"
1416
- return '""'
1417
-
1418
-
1419
- def _generate_edit_form_field(field_name: str, field_type: str, label: str) -> str:
1420
- """Generate a single form field for editing."""
1421
- type_lower = field_type.lower()
1422
-
1423
- if type_lower == "boolean":
1424
- return f""" <div className="mb-6">
1425
- <label className="flex items-center gap-3 cursor-pointer">
1426
- <input
1427
- type="checkbox"
1428
- checked={{{field_name}}}
1429
- onChange={{(e) => set{field_name.capitalize()}(e.target.checked)}}
1430
- className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-pointer transition-all duration-300"
1431
- />
1432
- <span className="text-slate-200 font-medium">{label}</span>
1433
- </label>
1434
- </div>"""
1435
- elif type_lower in ["number", "int", "integer", "float"]:
1436
- return f""" <div className="mb-6">
1437
- <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>
1438
- <input
1439
- type="number"
1440
- value={{{field_name}}}
1441
- onChange={{(e) => set{field_name.capitalize()}(parseFloat(e.target.value) || 0)}}
1442
- className="input-field"
1443
- />
1444
- </div>"""
1445
- else:
1446
- # String/text fields
1447
- return f""" <div className="mb-6">
1448
- <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>
1449
- <input
1450
- type="text"
1451
- value={{{field_name}}}
1452
- onChange={{(e) => set{field_name.capitalize()}(e.target.value)}}
1453
- className="input-field"
1454
- required
1455
- />
1456
- </div>"""
1457
-
1458
-
1459
- def generate_actions_component(resource_name: str) -> str:
1460
- """Generate the actions component for delete functionality.
1461
-
1462
- Args:
1463
- resource_name: Name of the resource (e.g., "todo", "product")
1464
-
1465
- Returns:
1466
- Complete TypeScript/React client component code
1467
- """
1468
- resource = resource_name.lower()
1469
- Resource = resource_name.capitalize()
1470
-
1471
- return CLIENT_COMPONENT_ACTIONS.format(resource=resource, Resource=Resource)
1472
-
1473
-
1474
- def _generate_detail_field_display(resource: str, fields: dict) -> str:
1475
- """Generate JSX for displaying resource fields in detail view.
1476
-
1477
- Boolean fields render as visual checkboxes with strikethrough for completed items.
1478
- Uses modern dark theme styling.
1479
-
1480
- Args:
1481
- resource: Resource variable name (e.g., "todo")
1482
- fields: Dictionary mapping field names to their types
1483
-
1484
- Returns:
1485
- JSX string for detail field display
1486
- """
1487
- display_fields = []
1488
-
1489
- # Find title and boolean fields for special combined rendering
1490
- title_field = None
1491
- boolean_field = None
1492
- for field_name, field_type in fields.items():
1493
- if field_name.lower() in {"title", "name"} and not title_field:
1494
- title_field = field_name
1495
- if field_type.lower() == "boolean" and not boolean_field:
1496
- boolean_field = field_name
1497
-
1498
- for field_name, field_type in fields.items():
1499
- if field_name.lower() in {"id", "createdat", "updatedat"}:
1500
- continue
1501
-
1502
- label = field_name.replace("_", " ").title()
1503
-
1504
- if field_type.lower() == "boolean":
1505
- # Render checkbox with visual feedback
1506
- display_fields.append(
1507
- f' <div className="mb-6 p-4 bg-slate-900/30 rounded-xl border border-slate-700/30">\n'
1508
- f' <label className="block text-sm font-medium text-slate-400 mb-3">{label}</label>\n'
1509
- f' <div className="flex items-center gap-3">\n'
1510
- f' <div className="relative">\n'
1511
- f" <input\n"
1512
- f' type="checkbox"\n'
1513
- f" checked={{{resource}.{field_name}}}\n"
1514
- f" readOnly\n"
1515
- f' className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-default transition-all duration-300"\n'
1516
- f" />\n"
1517
- f" {{{resource}.{field_name} && (\n"
1518
- f' <svg className="absolute inset-0 w-6 h-6 text-white pointer-events-none p-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n'
1519
- f' <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{3}} d="M5 13l4 4L19 7" />\n'
1520
- f" </svg>\n"
1521
- f" )}}\n"
1522
- f" </div>\n"
1523
- f' <span className={{{resource}.{field_name} ? "text-emerald-400 font-medium" : "text-slate-500"}}>\n'
1524
- f' {{{resource}.{field_name} ? "Yes" : "No"}}\n'
1525
- f" </span>\n"
1526
- f" </div>\n"
1527
- f" </div>"
1528
- )
1529
- elif field_type.lower() in ["date", "datetime", "timestamp"]:
1530
- display_fields.append(
1531
- f' <div className="mb-6">\n'
1532
- f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1533
- f' <p className="text-lg text-slate-200">{{new Date({resource}.{field_name}).toLocaleDateString()}}</p>\n'
1534
- f" </div>"
1535
- )
1536
- else:
1537
- # For title field with boolean, add strikethrough styling
1538
- if field_name == title_field and boolean_field:
1539
- class_expr = f'{{{resource}.{boolean_field} ? "text-xl text-slate-500 line-through" : "text-xl text-slate-100"}}'
1540
- display_fields.append(
1541
- f' <div className="mb-6">\n'
1542
- f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1543
- f" <p className={class_expr}>{{{resource}.{field_name}}}</p>\n"
1544
- f" </div>"
1545
- )
1546
- else:
1547
- display_fields.append(
1548
- f' <div className="mb-6">\n'
1549
- f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1550
- f' <p className="text-xl text-slate-100">{{{resource}.{field_name}}}</p>\n'
1551
- f" </div>"
1552
- )
1553
-
1554
- return (
1555
- "\n".join(display_fields)
1556
- if display_fields
1557
- else ' <p className="text-slate-400">No fields to display</p>'
1558
- )
1559
-
1560
-
1561
- def _map_type_to_typescript(field_type: str) -> str:
1562
- """Map field type to TypeScript type."""
1563
- type_mapping = {
1564
- "string": "string",
1565
- "text": "string",
1566
- "number": "number",
1567
- "float": "number",
1568
- "boolean": "boolean",
1569
- "date": "Date",
1570
- "datetime": "Date",
1571
- "timestamp": "Date",
1572
- "email": "string",
1573
- "url": "string",
1574
- }
1575
- return type_mapping.get(field_type.lower(), "string")
1576
-
1577
-
1578
- # ========== Test Templates (Vitest) ==========
1579
-
1580
- VITEST_CONFIG = """import { defineConfig } from 'vitest/config';
1581
- import react from '@vitejs/plugin-react';
1582
- import path from 'path';
1583
-
1584
- export default defineConfig({
1585
- plugins: [react()],
1586
- test: {
1587
- environment: 'jsdom',
1588
- globals: true,
1589
- setupFiles: ['./tests/setup.ts'],
1590
- include: ['**/__tests__/**/*.test.{ts,tsx}'],
1591
- coverage: {
1592
- provider: 'v8',
1593
- reporter: ['text', 'json', 'html'],
1594
- },
1595
- },
1596
- resolve: {
1597
- alias: {
1598
- '@': path.resolve(__dirname, './src'),
1599
- },
1600
- },
1601
- });
1602
- """
1603
-
1604
- TEST_SETUP = """import '@testing-library/jest-dom';
1605
- import {{ vi }} from 'vitest';
1606
-
1607
- // Mock next/navigation
1608
- vi.mock('next/navigation', () => ({{
1609
- useRouter: () => ({{
1610
- push: vi.fn(),
1611
- back: vi.fn(),
1612
- refresh: vi.fn(),
1613
- replace: vi.fn(),
1614
- }}),
1615
- usePathname: () => '/',
1616
- useSearchParams: () => new URLSearchParams(),
1617
- }}));
1618
-
1619
- // Mock Prisma client
1620
- vi.mock('@/lib/prisma', () => ({{
1621
- prisma: {{
1622
- {resource}: {{
1623
- findMany: vi.fn(),
1624
- findUnique: vi.fn(),
1625
- create: vi.fn(),
1626
- update: vi.fn(),
1627
- delete: vi.fn(),
1628
- count: vi.fn(),
1629
- }},
1630
- }},
1631
- }}));
1632
- """
1633
-
1634
-
1635
- COMPONENT_TEST_FORM = """import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
1636
- import {{ render, screen, fireEvent, waitFor }} from '@testing-library/react';
1637
- import {{ {Resource}Form }} from '../{Resource}Form';
1638
-
1639
- // Mock fetch
1640
- global.fetch = vi.fn();
1641
-
1642
- describe('{Resource}Form', () => {{
1643
- beforeEach(() => {{
1644
- vi.clearAllMocks();
1645
- (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1646
- ok: true,
1647
- json: () => Promise.resolve({{ id: 1 }}),
1648
- }});
1649
- }});
1650
-
1651
- it('renders all form fields', () => {{
1652
- render(<{Resource}Form />);
1653
-
1654
- {form_field_assertions}
1655
- }});
1656
-
1657
- it('submits form data correctly in create mode', async () => {{
1658
- render(<{Resource}Form mode="create" />);
1659
-
1660
- {form_fill_actions}
1661
-
1662
- fireEvent.click(screen.getByRole('button', {{ name: /create/i }}));
1663
-
1664
- await waitFor(() => {{
1665
- expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}', expect.objectContaining({{
1666
- method: 'POST',
1667
- headers: {{ 'Content-Type': 'application/json' }},
1668
- }}));
1669
- }});
1670
- }});
1671
-
1672
- it('submits form data correctly in edit mode', async () => {{
1673
- const initialData = {{ id: 1, {test_data_fields} }};
1674
- render(<{Resource}Form initialData={{initialData}} mode="edit" />);
1675
-
1676
- fireEvent.click(screen.getByRole('button', {{ name: /update/i }}));
1677
-
1678
- await waitFor(() => {{
1679
- expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}/1', expect.objectContaining({{
1680
- method: 'PATCH',
1681
- }}));
1682
- }});
1683
- }});
1684
-
1685
- it('displays error message on failed submission', async () => {{
1686
- (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1687
- ok: false,
1688
- json: () => Promise.resolve({{ error: 'Validation failed' }}),
1689
- }});
1690
-
1691
- render(<{Resource}Form />);
1692
-
1693
- {form_fill_actions}
1694
-
1695
- fireEvent.click(screen.getByRole('button', {{ name: /create/i }}));
1696
-
1697
- await waitFor(() => {{
1698
- expect(screen.getByText(/validation failed/i)).toBeInTheDocument();
1699
- }});
1700
- }});
1701
- }});
1702
- """
1703
-
1704
- COMPONENT_TEST_ACTIONS = """import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
1705
- import {{ render, screen, fireEvent, waitFor }} from '@testing-library/react';
1706
- import {{ {Resource}Actions }} from '../{Resource}Actions';
1707
-
1708
- // Mock fetch and confirm
1709
- global.fetch = vi.fn();
1710
- global.confirm = vi.fn();
1711
-
1712
- describe('{Resource}Actions', () => {{
1713
- beforeEach(() => {{
1714
- vi.clearAllMocks();
1715
- (global.confirm as ReturnType<typeof vi.fn>).mockReturnValue(true);
1716
- (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1717
- ok: true,
1718
- json: () => Promise.resolve({{ success: true }}),
1719
- }});
1720
- }});
1721
-
1722
- it('renders delete button', () => {{
1723
- render(<{Resource}Actions {resource}Id={{1}} />);
1724
-
1725
- expect(screen.getByRole('button', {{ name: /delete/i }})).toBeInTheDocument();
1726
- }});
1727
-
1728
- it('confirms before deleting', async () => {{
1729
- render(<{Resource}Actions {resource}Id={{1}} />);
1730
-
1731
- fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1732
-
1733
- expect(confirm).toHaveBeenCalled();
1734
- }});
1735
-
1736
- it('calls delete API on confirmation', async () => {{
1737
- render(<{Resource}Actions {resource}Id={{1}} />);
1738
-
1739
- fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1740
-
1741
- await waitFor(() => {{
1742
- expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}/1', {{
1743
- method: 'DELETE',
1744
- }});
1745
- }});
1746
- }});
1747
-
1748
- it('does not call API when delete is cancelled', () => {{
1749
- (global.confirm as ReturnType<typeof vi.fn>).mockReturnValue(false);
1750
-
1751
- render(<{Resource}Actions {resource}Id={{1}} />);
1752
-
1753
- fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1754
-
1755
- expect(fetch).not.toHaveBeenCalled();
1756
- }});
1757
-
1758
- it('displays error message on failed deletion', async () => {{
1759
- (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1760
- ok: false,
1761
- }});
1762
-
1763
- render(<{Resource}Actions {resource}Id={{1}} />);
1764
-
1765
- fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1766
-
1767
- await waitFor(() => {{
1768
- expect(screen.getByText(/failed to delete/i)).toBeInTheDocument();
1769
- }});
1770
- }});
1771
- }});
1772
- """
1773
-
1774
-
1775
- def generate_test_data_fields(fields: dict, variant: int = 1) -> str:
1776
- """Generate test data fields for test templates.
1777
-
1778
- Args:
1779
- fields: Dictionary of field names to types
1780
- variant: Variant number for different test data
1781
-
1782
- Returns:
1783
- String of test data field assignments
1784
- """
1785
- test_values = []
1786
- for field_name, field_type in fields.items():
1787
- if field_name in ["id", "createdAt", "updatedAt"]:
1788
- continue
1789
-
1790
- normalized_type = field_type.lower()
1791
- if normalized_type == "boolean":
1792
- value = "true" if variant == 1 else "false"
1793
- elif normalized_type in ["number", "int", "float"]:
1794
- value = str(variant * 10)
1795
- elif normalized_type == "email":
1796
- value = f'"test{variant}@example.com"'
1797
- elif normalized_type == "url":
1798
- value = f'"https://example{variant}.com"'
1799
- else:
1800
- value = f'"{field_name.title()} {variant}"'
1801
-
1802
- test_values.append(f"{field_name}: {value}")
1803
-
1804
- return ", ".join(test_values)
1805
-
1806
-
1807
- def generate_form_field_assertions(fields: dict) -> str:
1808
- """Generate test assertions for form field presence.
1809
-
1810
- Args:
1811
- fields: Dictionary of field names to types
1812
-
1813
- Returns:
1814
- String of expect statements
1815
- """
1816
- assertions = []
1817
- for field_name, field_type in fields.items():
1818
- if field_name in ["id", "createdAt", "updatedAt"]:
1819
- continue
1820
-
1821
- label = field_name.replace("_", " ").title()
1822
- if field_type.lower() == "boolean":
1823
- assertions.append(
1824
- f"expect(screen.getByLabelText(/{label}/i)).toBeInTheDocument();"
1825
- )
1826
- else:
1827
- assertions.append(
1828
- f"expect(screen.getByLabelText(/{label}/i)).toBeInTheDocument();"
1829
- )
1830
-
1831
- return "\n ".join(assertions)
1832
-
1833
-
1834
- def generate_form_fill_actions(fields: dict) -> str:
1835
- """Generate test actions to fill form fields.
1836
-
1837
- Args:
1838
- fields: Dictionary of field names to types
1839
-
1840
- Returns:
1841
- String of fireEvent calls
1842
- """
1843
- actions = []
1844
- for field_name, field_type in fields.items():
1845
- if field_name in ["id", "createdAt", "updatedAt"]:
1846
- continue
1847
-
1848
- label = field_name.replace("_", " ").title()
1849
- normalized_type = field_type.lower()
1850
-
1851
- if normalized_type == "boolean":
1852
- actions.append(f"fireEvent.click(screen.getByLabelText(/{label}/i));")
1853
- else:
1854
- test_value = "Test Value"
1855
- if normalized_type in ["number", "int", "float"]:
1856
- test_value = "42"
1857
- elif normalized_type == "email":
1858
- test_value = "test@example.com"
1859
- elif normalized_type == "url":
1860
- test_value = "https://example.com"
1861
-
1862
- actions.append(
1863
- f"fireEvent.change(screen.getByLabelText(/{label}/i), {{ target: {{ value: '{test_value}' }} }});"
1864
- )
1865
-
1866
- return "\n ".join(actions)
1867
-
1868
-
1869
- # ========== Style Test Templates (Issue #1002) ==========
1870
-
1871
- STYLE_TEST_TEMPLATE = """import { describe, it, expect, beforeAll } from 'vitest';
1872
- import * as fs from 'fs';
1873
- import * as path from 'path';
1874
-
1875
- describe('Global CSS Integrity', () => {
1876
- const globalsPath = path.join(process.cwd(), 'src/app/globals.css');
1877
- let cssContent: string;
1878
-
1879
- beforeAll(() => {
1880
- cssContent = fs.readFileSync(globalsPath, 'utf-8');
1881
- });
1882
-
1883
- describe('File Content Type (CRITICAL - Issue #1002)', () => {
1884
- it('is valid CSS, not TypeScript/JavaScript', () => {
1885
- // These patterns indicate wrong file content - always invalid
1886
- expect(cssContent).not.toMatch(/^\\s*import\\s+.*from/m);
1887
- expect(cssContent).not.toMatch(/^\\s*export\\s+(default|const|function|class)/m);
1888
- expect(cssContent).not.toMatch(/"use client"|'use client'/);
1889
- expect(cssContent).not.toMatch(/^\\s*interface\\s+\\w+/m);
1890
- expect(cssContent).not.toMatch(/^\\s*type\\s+\\w+\\s*=/m);
1891
- expect(cssContent).not.toMatch(/<[A-Z][a-zA-Z]*[\\s/>]/); // JSX tags
1892
- });
1893
-
1894
- it('has balanced CSS braces', () => {
1895
- const open = (cssContent.match(/\\{/g) || []).length;
1896
- const close = (cssContent.match(/\\}/g) || []).length;
1897
- expect(open).toBe(close);
1898
- });
1899
- });
1900
-
1901
- describe('Tailwind Framework', () => {
1902
- it('includes Tailwind directives', () => {
1903
- // At minimum, CSS should have Tailwind setup
1904
- const hasTailwind =
1905
- cssContent.includes('@tailwind') ||
1906
- cssContent.includes('@import "tailwindcss');
1907
- expect(hasTailwind).toBe(true);
1908
- });
1909
- });
1910
-
1911
- describe('Design System Classes', () => {
1912
- it('defines glass-card class', () => {
1913
- expect(cssContent).toContain('.glass-card');
1914
- });
1915
-
1916
- it('defines btn-primary class', () => {
1917
- expect(cssContent).toContain('.btn-primary');
1918
- });
1919
-
1920
- it('defines page-title class', () => {
1921
- expect(cssContent).toContain('.page-title');
1922
- });
1923
- });
1924
- });
1925
- """
1926
-
1927
- ROUTES_TEST_TEMPLATE = """import {{ describe, it, expect }} from 'vitest';
1928
- import * as fs from 'fs';
1929
- import * as path from 'path';
1930
- import {{ glob }} from 'glob';
1931
-
1932
- describe('Next.js App Router Structure', () => {{
1933
- const appDir = path.join(process.cwd(), 'src/app');
1934
-
1935
- describe('Root Layout (Global Styles Entry Point)', () => {{
1936
- it('layout.tsx exists', () => {{
1937
- const layoutPath = path.join(appDir, 'layout.tsx');
1938
- expect(fs.existsSync(layoutPath)).toBe(true);
1939
- }});
1940
-
1941
- it('layout imports globals.css', () => {{
1942
- const layoutPath = path.join(appDir, 'layout.tsx');
1943
- const content = fs.readFileSync(layoutPath, 'utf-8');
1944
- // Should import globals.css (various import patterns)
1945
- expect(content).toMatch(/import\\s+['"]\\.\\/globals\\.css['"]|import\\s+['"]@\\/app\\/globals\\.css['"]/);
1946
- }});
1947
- }});
1948
-
1949
- describe('Page Structure', () => {{
1950
- it('all page.tsx files are valid React components', () => {{
1951
- const pages = glob.sync('**/page.tsx', {{ cwd: appDir }});
1952
-
1953
- for (const page of pages) {{
1954
- const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1955
-
1956
- // Should have an export (default or named)
1957
- expect(content).toMatch(/export\\s+(default\\s+)?(async\\s+)?function|export\\s+default/);
1958
-
1959
- // Should not be empty
1960
- expect(content.trim().length).toBeGreaterThan(50);
1961
- }}
1962
- }});
1963
-
1964
- it('dynamic routes have params handling', () => {{
1965
- const dynamicPages = glob.sync('**/\\\\[*\\\\]/**/page.tsx', {{ cwd: appDir }});
1966
-
1967
- for (const page of dynamicPages) {{
1968
- const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1969
- // Should reference params somewhere
1970
- expect(content).toMatch(/params|searchParams/);
1971
- }}
1972
- }});
1973
- }});
1974
-
1975
- describe('Styling Consistency', () => {{
1976
- it('pages use className attributes (styled, not unstyled)', () => {{
1977
- const pages = glob.sync('**/page.tsx', {{ cwd: appDir, ignore: '**/api/**' }});
1978
-
1979
- for (const page of pages) {{
1980
- const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1981
- // Each page should have some styling
1982
- const classNameCount = (content.match(/className=/g) || []).length;
1983
- expect(classNameCount).toBeGreaterThan(0);
1984
- }}
1985
- }});
1986
- }});
1987
-
1988
- describe('API Routes Exist', () => {{
1989
- it('has API routes for CRUD operations', () => {{
1990
- const apiRoutes = glob.sync('**/route.ts', {{ cwd: path.join(appDir, 'api') }});
1991
- expect(apiRoutes.length).toBeGreaterThan(0);
1992
- }});
1993
-
1994
- it('API routes export HTTP methods', () => {{
1995
- const apiDir = path.join(appDir, 'api');
1996
- if (!fs.existsSync(apiDir)) return; // Skip if no API dir
1997
-
1998
- const apiRoutes = glob.sync('**/route.ts', {{ cwd: apiDir }});
1999
-
2000
- for (const route of apiRoutes) {{
2001
- const content = fs.readFileSync(path.join(apiDir, route), 'utf-8');
2002
- // Should export at least one HTTP method
2003
- expect(content).toMatch(/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE)/);
2004
- }}
2005
- }});
2006
- }});
2007
- }});
2008
- """
2009
-
2010
-
2011
- def generate_style_test_content(_resource_name: str = "Item") -> str:
2012
- """Generate the content for styles.test.ts.
2013
-
2014
- Args:
2015
- _resource_name: Resource name for component checks (unused, kept for API compatibility)
2016
-
2017
- Returns:
2018
- Complete test file content
2019
- """
2020
- return STYLE_TEST_TEMPLATE
2021
-
2022
-
2023
- def generate_routes_test_content(resource_name: str = "Item") -> str:
2024
- """Generate the content for routes.test.ts.
2025
-
2026
- Args:
2027
- resource_name: Resource name for route checks
2028
-
2029
- Returns:
2030
- Complete test file content
2031
- """
2032
- return ROUTES_TEST_TEMPLATE.format(
2033
- resource=resource_name.lower(),
2034
- Resource=resource_name.capitalize(),
2035
- resource_plural=pluralize(resource_name.lower()),
2036
- )
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """Code generation patterns for web applications.
4
+
5
+ This module contains reusable code patterns for generating functional
6
+ web application code. Patterns are framework-agnostic where possible,
7
+ with framework-specific variants where needed.
8
+
9
+ Patterns are stored as template strings that can be formatted with
10
+ resource-specific context (model names, fields, etc.).
11
+ """
12
+
13
+ # ========== App-Wide Layout and Styling ==========
14
+
15
+ APP_LAYOUT = """import type {{ Metadata }} from "next";
16
+ import {{ Inter }} from "next/font/google";
17
+ import "./globals.css";
18
+
19
+ const inter = Inter({{ subsets: ["latin"] }});
20
+
21
+ export const metadata: Metadata = {{
22
+ title: "{app_title}",
23
+ description: "{app_description}",
24
+ }};
25
+
26
+ export default function RootLayout({{
27
+ children,
28
+ }}: Readonly<{{
29
+ children: React.ReactNode;
30
+ }}>) {{
31
+ return (
32
+ <html lang="en" className="dark">
33
+ <body className={{`${{inter.className}} antialiased`}}>
34
+ <div className="fixed inset-0 bg-gradient-to-br from-slate-900 via-purple-900/20 to-slate-900 -z-10" />
35
+ <div className="fixed inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-900/20 via-transparent to-transparent -z-10" />
36
+ {{children}}
37
+ </body>
38
+ </html>
39
+ );
40
+ }}"""
41
+
42
+ APP_GLOBALS_CSS = """@tailwind base;
43
+ @tailwind components;
44
+ @tailwind utilities;
45
+
46
+ :root {{
47
+ --background: #0f0f1a;
48
+ --foreground: #e2e8f0;
49
+ }}
50
+
51
+ body {{
52
+ color: var(--foreground);
53
+ background: var(--background);
54
+ min-height: 100vh;
55
+ }}
56
+
57
+ /* Custom scrollbar */
58
+ ::-webkit-scrollbar {{
59
+ width: 8px;
60
+ }}
61
+
62
+ ::-webkit-scrollbar-track {{
63
+ background: rgba(30, 41, 59, 0.3);
64
+ }}
65
+
66
+ ::-webkit-scrollbar-thumb {{
67
+ background: rgba(99, 102, 241, 0.5);
68
+ border-radius: 4px;
69
+ }}
70
+
71
+ ::-webkit-scrollbar-thumb:hover {{
72
+ background: rgba(99, 102, 241, 0.7);
73
+ }}
74
+
75
+ @layer base {{
76
+ /* Dark mode color scheme */
77
+ html {{
78
+ color-scheme: dark;
79
+ }}
80
+
81
+ /* Better form defaults for dark theme */
82
+ input[type="checkbox"] {{
83
+ color-scheme: dark;
84
+ }}
85
+ }}
86
+
87
+ @layer components {{
88
+ /* Glass card effect */
89
+ .glass-card {{
90
+ @apply bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl;
91
+ }}
92
+
93
+ /* Button variants */
94
+ .btn-primary {{
95
+ @apply bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium
96
+ hover:from-indigo-600 hover:to-purple-600 transition-all duration-300
97
+ hover:shadow-lg hover:shadow-indigo-500/25 active:scale-95 disabled:opacity-50;
98
+ }}
99
+
100
+ .btn-secondary {{
101
+ @apply bg-slate-700/50 text-slate-200 px-6 py-3 rounded-xl font-medium border border-slate-600/50
102
+ hover:bg-slate-700 transition-all duration-300 active:scale-95;
103
+ }}
104
+
105
+ .btn-danger {{
106
+ @apply bg-gradient-to-r from-red-500 to-rose-500 text-white px-6 py-3 rounded-xl font-medium
107
+ hover:from-red-600 hover:to-rose-600 transition-all duration-300
108
+ hover:shadow-lg hover:shadow-red-500/25 active:scale-95 disabled:opacity-50;
109
+ }}
110
+
111
+ /* Input styling */
112
+ .input-field {{
113
+ @apply w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl
114
+ text-slate-100 placeholder-slate-500
115
+ focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50
116
+ transition-all duration-300;
117
+ }}
118
+
119
+ /* Modern checkbox */
120
+ .checkbox-modern {{
121
+ @apply appearance-none w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50
122
+ checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500
123
+ checked:border-transparent cursor-pointer transition-all duration-300
124
+ hover:border-indigo-400 focus:ring-2 focus:ring-indigo-500/50;
125
+ }}
126
+
127
+ /* Page title with gradient */
128
+ .page-title {{
129
+ @apply text-4xl font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400
130
+ bg-clip-text text-transparent;
131
+ }}
132
+
133
+ /* Back link styling */
134
+ .link-back {{
135
+ @apply inline-flex items-center gap-2 text-slate-400 hover:text-indigo-400
136
+ transition-colors duration-300;
137
+ }}
138
+ }}
139
+
140
+ @layer utilities {{
141
+ .text-balance {{
142
+ text-wrap: balance;
143
+ }}
144
+ }}
145
+ """
146
+
147
+ # ========== Landing Page Pattern ==========
148
+
149
+ LANDING_PAGE_WITH_LINKS = """import Link from "next/link";
150
+
151
+ export default function Home() {{
152
+ return (
153
+ <main className="min-h-screen">
154
+ <div className="container mx-auto px-4 py-12 max-w-4xl">
155
+ <h1 className="page-title mb-8">Welcome</h1>
156
+
157
+ <div className="grid gap-6">
158
+ <Link
159
+ href="/{resource_plural}"
160
+ className="glass-card p-6 block hover:border-indigo-500/50 transition-all duration-300 group"
161
+ >
162
+ <div className="flex items-center justify-between">
163
+ <div>
164
+ <h2 className="text-2xl font-semibold text-slate-100 mb-2 group-hover:text-indigo-400 transition-colors">{Resource}s</h2>
165
+ <p className="text-slate-400">{link_description}</p>
166
+ </div>
167
+ <svg className="w-6 h-6 text-slate-500 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M9 5l7 7-7 7" />
169
+ </svg>
170
+ </div>
171
+ </Link>
172
+ </div>
173
+ </div>
174
+ </main>
175
+ );
176
+ }}
177
+ """
178
+
179
+ # ========== API Route Patterns (Next.js) ==========
180
+
181
+ API_ROUTE_GET = """export async function GET() {{
182
+ try {{
183
+ const {resource_plural} = await prisma.{resource}.findMany({{
184
+ orderBy: {{ id: 'desc' }},
185
+ take: 50
186
+ }});
187
+ return NextResponse.json({resource_plural});
188
+ }} catch (error) {{
189
+ console.error('GET /{resource}s error:', error);
190
+ return NextResponse.json(
191
+ {{ error: 'Failed to fetch {resource}s' }},
192
+ {{ status: 500 }}
193
+ );
194
+ }}
195
+ }}"""
196
+
197
+ API_ROUTE_GET_PAGINATED = """export async function GET(request: Request) {{
198
+ try {{
199
+ const {{ searchParams }} = new URL(request.url);
200
+ const page = parseInt(searchParams.get('page') || '1');
201
+ const limit = parseInt(searchParams.get('limit') || '10');
202
+ const skip = (page - 1) * limit;
203
+
204
+ const [{resource_plural}, total] = await Promise.all([
205
+ prisma.{resource}.findMany({{
206
+ skip,
207
+ take: limit,
208
+ orderBy: {{ id: 'desc' }}
209
+ }}),
210
+ prisma.{resource}.count()
211
+ ]);
212
+
213
+ return NextResponse.json({{
214
+ {resource_plural},
215
+ pagination: {{
216
+ page,
217
+ limit,
218
+ total,
219
+ pages: Math.ceil(total / limit)
220
+ }}
221
+ }});
222
+ }} catch (error) {{
223
+ console.error('GET /{resource}s error:', error);
224
+ return NextResponse.json(
225
+ {{ error: 'Failed to fetch {resource}s' }},
226
+ {{ status: 500 }}
227
+ );
228
+ }}
229
+ }}"""
230
+
231
+ API_ROUTE_POST = """export async function POST(request: Request) {{
232
+ try {{
233
+ const body = await request.json();
234
+
235
+ // Validate request body
236
+ const validatedData = {Resource}Schema.parse(body);
237
+
238
+ const {resource} = await prisma.{resource}.create({{
239
+ data: validatedData
240
+ }});
241
+
242
+ return NextResponse.json({resource}, {{ status: 201 }});
243
+ }} catch (error) {{
244
+ if (error instanceof z.ZodError) {{
245
+ return NextResponse.json(
246
+ {{ error: 'Invalid request data', details: error.issues }},
247
+ {{ status: 400 }}
248
+ );
249
+ }}
250
+
251
+ console.error('POST /{resource}s error:', error);
252
+ return NextResponse.json(
253
+ {{ error: 'Failed to create {resource}' }},
254
+ {{ status: 500 }}
255
+ );
256
+ }}
257
+ }}"""
258
+
259
+ API_ROUTE_DYNAMIC_GET = """export async function GET(
260
+ request: Request,
261
+ {{ params }}: {{ params: {{ id: string }} }}
262
+ ) {{
263
+ try {{
264
+ const id = parseInt(params.id);
265
+
266
+ const {resource} = await prisma.{resource}.findUnique({{
267
+ where: {{ id }}
268
+ }});
269
+
270
+ if (!{resource}) {{
271
+ return NextResponse.json(
272
+ {{ error: '{Resource} not found' }},
273
+ {{ status: 404 }}
274
+ );
275
+ }}
276
+
277
+ return NextResponse.json({resource});
278
+ }} catch (error) {{
279
+ console.error('GET /{resource}/[id] error:', error);
280
+ return NextResponse.json(
281
+ {{ error: 'Failed to fetch {resource}' }},
282
+ {{ status: 500 }}
283
+ );
284
+ }}
285
+ }}"""
286
+
287
+ API_ROUTE_DYNAMIC_PATCH = """export async function PATCH(
288
+ request: Request,
289
+ {{ params }}: {{ params: {{ id: string }} }}
290
+ ) {{
291
+ try {{
292
+ const id = parseInt(params.id);
293
+ const body = await request.json();
294
+
295
+ const validatedData = {Resource}UpdateSchema.parse(body);
296
+
297
+ const {resource} = await prisma.{resource}.update({{
298
+ where: {{ id }},
299
+ data: validatedData
300
+ }});
301
+
302
+ return NextResponse.json({resource});
303
+ }} catch (error) {{
304
+ if (error instanceof z.ZodError) {{
305
+ return NextResponse.json(
306
+ {{ error: 'Invalid update data', details: error.issues }},
307
+ {{ status: 400 }}
308
+ );
309
+ }}
310
+
311
+ console.error('PATCH /{resource}/[id] error:', error);
312
+ return NextResponse.json(
313
+ {{ error: 'Failed to update {resource}' }},
314
+ {{ status: 500 }}
315
+ );
316
+ }}
317
+ }}"""
318
+
319
+ API_ROUTE_DYNAMIC_DELETE = """export async function DELETE(
320
+ request: Request,
321
+ {{ params }}: {{ params: {{ id: string }} }}
322
+ ) {{
323
+ try {{
324
+ const id = parseInt(params.id);
325
+
326
+ await prisma.{resource}.delete({{
327
+ where: {{ id }}
328
+ }});
329
+
330
+ return NextResponse.json({{ success: true }});
331
+ }} catch (error) {{
332
+ console.error('DELETE /{resource}/[id] error:', error);
333
+ return NextResponse.json(
334
+ {{ error: 'Failed to delete {resource}' }},
335
+ {{ status: 500 }}
336
+ );
337
+ }}
338
+ }}"""
339
+
340
+ # ========== Validation Schema Patterns ==========
341
+
342
+
343
+ def generate_zod_schema(resource_name: str, fields: dict) -> str:
344
+ """Generate Zod validation schema for a resource.
345
+
346
+ Args:
347
+ resource_name: Name of the resource (e.g., "todo", "user")
348
+ fields: Dictionary of field names to types
349
+
350
+ Returns:
351
+ TypeScript code for Zod schema
352
+ """
353
+ schema_fields = []
354
+ for field_name, field_type in fields.items():
355
+ if field_name in ["id", "createdAt", "updatedAt"]:
356
+ continue # Skip auto-generated fields
357
+
358
+ zod_type = _map_type_to_zod(field_type)
359
+ schema_fields.append(f" {field_name}: {zod_type}")
360
+
361
+ resource_capitalized = resource_name.capitalize()
362
+
363
+ return f"""const {resource_capitalized}Schema = z.object({{
364
+ {','.join(schema_fields)}
365
+ }});
366
+
367
+ const {resource_capitalized}UpdateSchema = {resource_capitalized}Schema.partial();
368
+
369
+ type {resource_capitalized} = z.infer<typeof {resource_capitalized}Schema>;"""
370
+
371
+
372
+ def _map_type_to_zod(field_type: str) -> str:
373
+ """Map field type to Zod validation type."""
374
+ # Normalize to lowercase for consistent lookup
375
+ normalized = field_type.lower()
376
+
377
+ type_mapping = {
378
+ "string": "z.string().min(1)",
379
+ "text": "z.string()",
380
+ "int": "z.number().int()",
381
+ "number": "z.number().int()",
382
+ "float": "z.number()",
383
+ "boolean": "z.boolean()",
384
+ "date": "z.coerce.date()",
385
+ "datetime": "z.coerce.date()",
386
+ "timestamp": "z.coerce.date()",
387
+ "email": "z.string().email()",
388
+ "url": "z.string().url()",
389
+ }
390
+ return type_mapping.get(normalized, "z.string()")
391
+
392
+
393
+ # ========== React Component Patterns ==========
394
+
395
+ SERVER_COMPONENT_LIST = """import {{ prisma }} from "@/lib/prisma";
396
+ import Link from "next/link";
397
+ // EXTRA COMPONENT NOTE: Import any previously generated components/helpers as needed.
398
+ // import {{ AdditionalComponent }} from "@/components/AdditionalComponent";
399
+
400
+ async function get{Resource}s() {{
401
+ const {resource_plural} = await prisma.{resource}.findMany({{
402
+ orderBy: {{ id: "desc" }},
403
+ take: 50
404
+ }});
405
+ return {resource_plural};
406
+ }}
407
+
408
+ export default async function {Resource}sPage() {{
409
+ const {resource_plural} = await get{Resource}s();
410
+
411
+ return (
412
+ <div className="min-h-screen">
413
+ <div className="container mx-auto px-4 py-12 max-w-4xl">
414
+ {{/* Header + Custom Components */}}
415
+ <div className="mb-10 flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
416
+ <div>
417
+ <h1 className="text-4xl font-bold bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent mb-2">
418
+ {Resource}s
419
+ </h1>
420
+ <p className="text-slate-400">
421
+ {{{resource_plural}.length === 0
422
+ ? "No items yet. Create your first one!"
423
+ : `${{({resource_plural} as any[]).filter(t => !(t as any).completed).length}} pending items`}}
424
+ </p>
425
+ </div>
426
+
427
+ {{/* EXTRA COMPONENT NOTE:
428
+ Check the plan for other generated components (timer, stats badge, etc.)
429
+ and render them here via their imports. Example:
430
+ <AdditionalComponent targetTimestamp={{...}} />
431
+ Remove this placeholder when no extra component is needed. */}}
432
+ {{/* <AdditionalComponent className="w-full md:w-60" /> */}}
433
+ </div>
434
+
435
+ {{/* Add Button */}}
436
+ <div className="mb-8">
437
+ <Link
438
+ href="/{resource}s/new"
439
+ className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300 hover:shadow-lg hover:shadow-indigo-500/25"
440
+ >
441
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
442
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M12 4v16m8-8H4" />
443
+ </svg>
444
+ Add New {Resource}
445
+ </Link>
446
+ </div>
447
+
448
+ {{/* List */}}
449
+ <div className="bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 rounded-2xl shadow-2xl p-6">
450
+ {{{resource_plural}.length === 0 ? (
451
+ <div className="text-center py-16">
452
+ <div className="w-20 h-20 mx-auto mb-6 rounded-full bg-slate-800/50 flex items-center justify-center">
453
+ <svg className="w-10 h-10 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
454
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{1.5}} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
455
+ </svg>
456
+ </div>
457
+ <h3 className="text-xl font-medium text-slate-300 mb-2">No {resource}s yet</h3>
458
+ <p className="text-slate-500 mb-6">Create your first item to get started</p>
459
+ <Link
460
+ href="/{resource}s/new"
461
+ className="inline-flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-500 text-white px-6 py-3 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300"
462
+ >
463
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
464
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M12 4v16m8-8H4" />
465
+ </svg>
466
+ Create {Resource}
467
+ </Link>
468
+ </div>
469
+ ) : (
470
+ <div className="space-y-3">
471
+ {{{resource_plural}.map((item) => (
472
+ <Link
473
+ key={{item.id}}
474
+ href={{`/{resource}s/${{item.id}}`}}
475
+ className="block p-5 rounded-xl bg-slate-800/30 border border-slate-700/30 hover:bg-slate-800/50 hover:border-indigo-500/30 transition-all duration-300"
476
+ >
477
+ <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
478
+ <div className="flex-1">{field_display}</div>
479
+ {{/* EXTRA COMPONENT NOTE:
480
+ Check the plan for per-item components that were generated (countdown,
481
+ status badge, etc.) and include them here. Example:
482
+ <AdditionalComponent targetTimestamp={{item.missionTime}} />
483
+ Remove this placeholder if no extra component is needed. */}}
484
+ {{/* <AdditionalComponent {...item} className="w-full md:w-56" /> */}}
485
+ </div>
486
+ </Link>
487
+ ))}}
488
+ </div>
489
+ )}}
490
+ </div>
491
+ </div>
492
+ </div>
493
+ );
494
+ }}"""
495
+
496
+ CLIENT_COMPONENT_FORM = """"use client";
497
+
498
+ import {{ useState, useEffect }} from "react";
499
+ import {{ useRouter }} from "next/navigation";
500
+ import type {{ {Resource} }} from "@prisma/client";
501
+
502
+ interface {Resource}FormProps {{
503
+ initialData?: Partial<{Resource}>;
504
+ mode?: "create" | "edit";
505
+ }}
506
+
507
+ export function {Resource}Form({{ initialData, mode = "create" }}: {Resource}FormProps) {{
508
+ const router = useRouter();
509
+ const [loading, setLoading] = useState(false);
510
+ const [error, setError] = useState<string | null>(null);
511
+
512
+ const [formData, setFormData] = useState({{
513
+ {form_state_fields}
514
+ }});
515
+ const dateFields = {date_fields};
516
+
517
+ const normalizePayload = (data: typeof formData) => {{
518
+ if (dateFields.length === 0) {{
519
+ return data;
520
+ }}
521
+
522
+ const normalized = {{ ...data }};
523
+
524
+ dateFields.forEach((field) => {{
525
+ const value = normalized[field as keyof typeof normalized];
526
+ if (!value) {{
527
+ return;
528
+ }}
529
+
530
+ const parsedValue = new Date(value as string | number | Date);
531
+ if (!Number.isNaN(parsedValue.getTime())) {{
532
+ (normalized as any)[field] = parsedValue.toISOString();
533
+ }}
534
+ }});
535
+
536
+ return normalized;
537
+ }};
538
+
539
+ // Initialize form with initialData when in edit mode
540
+ useEffect(() => {{
541
+ if (initialData && mode === "edit") {{
542
+ setFormData(prev => ({{
543
+ ...prev,
544
+ ...Object.fromEntries(
545
+ Object.entries(initialData).filter(([key]) =>
546
+ !["id", "createdAt", "updatedAt"].includes(key)
547
+ )
548
+ )
549
+ }}));
550
+ }}
551
+ }}, [initialData, mode]);
552
+
553
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {{
554
+ const {{ name, value, type }} = e.target;
555
+ const checked = (e.target as HTMLInputElement).checked;
556
+
557
+ setFormData(prev => ({{
558
+ ...prev,
559
+ [name]: type === "checkbox" ? checked : type === "number" ? parseFloat(value) : value
560
+ }}));
561
+ }};
562
+
563
+ const handleSubmit = async (e: React.FormEvent) => {{
564
+ e.preventDefault();
565
+ setLoading(true);
566
+ setError(null);
567
+
568
+ try {{
569
+ const url = mode === "create"
570
+ ? "/api/{resource}s"
571
+ : `/api/{resource}s/${{initialData?.id}}`;
572
+
573
+ const method = mode === "create" ? "POST" : "PATCH";
574
+ const payload = normalizePayload(formData);
575
+
576
+ const response = await fetch(url, {{
577
+ method,
578
+ headers: {{ "Content-Type": "application/json" }},
579
+ body: JSON.stringify(payload)
580
+ }});
581
+
582
+ if (!response.ok) {{
583
+ const data = await response.json();
584
+ throw new Error(data.error || "Operation failed");
585
+ }}
586
+
587
+ router.push("/{resource}s");
588
+ router.refresh();
589
+ }} catch (err) {{
590
+ setError(err instanceof Error ? err.message : "An error occurred");
591
+ }} finally {{
592
+ setLoading(false);
593
+ }}
594
+ }};
595
+
596
+ return (
597
+ <form onSubmit={{handleSubmit}} className="space-y-6">
598
+ {form_fields}
599
+
600
+ {{error && (
601
+ <div className="bg-red-500/10 border border-red-500/20 text-red-400 p-4 rounded-xl">
602
+ {{error}}
603
+ </div>
604
+ )}}
605
+
606
+ <div className="flex gap-4 pt-4">
607
+ <button
608
+ type="submit"
609
+ disabled={{loading}}
610
+ className="flex-1 bg-gradient-to-r from-indigo-500 to-purple-500 text-white py-3 px-6 rounded-xl font-medium hover:from-indigo-600 hover:to-purple-600 transition-all duration-300 hover:shadow-lg hover:shadow-indigo-500/25 disabled:opacity-50 disabled:cursor-not-allowed"
611
+ >
612
+ {{loading ? "Saving..." : mode === "create" ? "Create {Resource}" : "Save Changes"}}
613
+ </button>
614
+ <button
615
+ type="button"
616
+ onClick={{() => router.back()}}
617
+ className="px-6 py-3 bg-slate-700/50 text-slate-200 rounded-xl font-medium border border-slate-600/50 hover:bg-slate-700 transition-all duration-300"
618
+ >
619
+ Cancel
620
+ </button>
621
+ </div>
622
+ </form>
623
+ );
624
+ }}"""
625
+
626
+ CLIENT_COMPONENT_TIMER = """"use client";
627
+
628
+ import {{ useEffect, useMemo, useState }} from "react";
629
+
630
+ interface {{ComponentName}}Props {{
631
+ targetTimestamp?: string; // ISO 8601 string that marks when the countdown ends
632
+ durationSeconds?: number; // Fallback duration (seconds) when no timestamp is provided
633
+ className?: string;
634
+ }}
635
+
636
+ const MS_IN_SECOND = 1000;
637
+ const MS_IN_MINUTE = 60 * MS_IN_SECOND;
638
+ const MS_IN_HOUR = 60 * MS_IN_MINUTE;
639
+ const MS_IN_DAY = 24 * MS_IN_HOUR;
640
+
641
+ export function {{ComponentName}}({{
642
+ targetTimestamp,
643
+ durationSeconds = 0,
644
+ className = "",
645
+ }}: {{ComponentName}}Props) {{
646
+ const deadlineMs = useMemo(() => {{
647
+ if (targetTimestamp) {{
648
+ const parsed = Date.parse(targetTimestamp);
649
+ return Number.isNaN(parsed) ? null : parsed;
650
+ }}
651
+ if (durationSeconds > 0) {{
652
+ return Date.now() + durationSeconds * MS_IN_SECOND;
653
+ }}
654
+ return null;
655
+ }}, [targetTimestamp, durationSeconds]);
656
+
657
+ const [timeLeftMs, setTimeLeftMs] = useState(() => {{
658
+ if (!deadlineMs) return 0;
659
+ return Math.max(deadlineMs - Date.now(), 0);
660
+ }});
661
+
662
+ useEffect(() => {{
663
+ if (!deadlineMs) {{
664
+ setTimeLeftMs(0);
665
+ return;
666
+ }}
667
+
668
+ const update = () => {{
669
+ setTimeLeftMs(Math.max(deadlineMs - Date.now(), 0));
670
+ }};
671
+
672
+ update();
673
+
674
+ const intervalId = window.setInterval(() => {{
675
+ update();
676
+ if (deadlineMs <= Date.now()) {{
677
+ window.clearInterval(intervalId);
678
+ }}
679
+ }}, 1000);
680
+
681
+ return () => window.clearInterval(intervalId);
682
+ }}, [deadlineMs]);
683
+
684
+ const isExpired = timeLeftMs <= 0;
685
+
686
+ // TIMER_NOTE: derive whichever granularity the feature demands (days, hours,
687
+ // minutes, seconds, milliseconds, etc.). Remove unused helpers so the final
688
+ // output matches the spec exactly.
689
+ const days = Math.floor(timeLeftMs / MS_IN_DAY);
690
+ const hours = Math.floor((timeLeftMs % MS_IN_DAY) / MS_IN_HOUR);
691
+ const minutes = Math.floor((timeLeftMs % MS_IN_HOUR) / MS_IN_MINUTE);
692
+ const seconds = Math.floor((timeLeftMs % MS_IN_MINUTE) / MS_IN_SECOND);
693
+
694
+ return (
695
+ <section
696
+ className={{`glass-card p-6 space-y-4 ${{className}}`.trim()}}
697
+ data-countdown-target={{targetTimestamp || ""}}
698
+ >
699
+ {{/* TIMER_NOTE: swap this placeholder layout for the requested display.
700
+ Emit only the units the user cares about (e.g., just minutes/seconds,
701
+ or a full days→hours→minutes breakdown). */}}
702
+ <div className="font-mono text-4xl text-slate-100">
703
+ {{seconds}}s
704
+ </div>
705
+
706
+ {{isExpired && (
707
+ <p className="text-sm text-slate-400">
708
+ {{/* TIMER_NOTE: replace this placeholder with the exact completion
709
+ copy or follow-up action the prompt describes. */}}
710
+ Countdown complete.
711
+ </p>
712
+ )}}
713
+ </section>
714
+ );
715
+ }}"""
716
+
717
+
718
+ CLIENT_COMPONENT_NEW_PAGE = """"use client";
719
+
720
+ import {{ {Resource}Form }} from "@/components/{Resource}Form";
721
+ import Link from "next/link";
722
+
723
+ export default function New{Resource}Page() {{
724
+ return (
725
+ <div className="min-h-screen">
726
+ <div className="container mx-auto px-4 py-12 max-w-2xl">
727
+ <div className="mb-8">
728
+ <Link
729
+ href="/{resource}s"
730
+ className="link-back group"
731
+ >
732
+ <svg className="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
733
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M15 19l-7-7 7-7" />
734
+ </svg>
735
+ Back to {Resource}s
736
+ </Link>
737
+ </div>
738
+
739
+ <div className="glass-card p-8">
740
+ <h1 className="page-title mb-8">
741
+ Create New {Resource}
742
+ </h1>
743
+
744
+ <{Resource}Form mode="create" />
745
+ </div>
746
+ </div>
747
+ </div>
748
+ );
749
+ }}"""
750
+
751
+ SERVER_COMPONENT_DETAIL = """"use client";
752
+
753
+ import {{ useRouter }} from "next/navigation";
754
+ import {{ useState, useEffect }} from "react";
755
+ import Link from "next/link";
756
+
757
+ interface {Resource}Data {{
758
+ id: number;
759
+ {interface_fields}
760
+ createdAt: string;
761
+ updatedAt: string;
762
+ }}
763
+
764
+ export default function {Resource}EditPage({{
765
+ params
766
+ }}: {{
767
+ params: {{ id: string }}
768
+ }}) {{
769
+ const router = useRouter();
770
+ const id = parseInt(params.id);
771
+ const [loading, setLoading] = useState(true);
772
+ const [saving, setSaving] = useState(false);
773
+ const [deleting, setDeleting] = useState(false);
774
+ const [error, setError] = useState<string | null>(null);
775
+ const [{resource}, set{Resource}] = useState<{Resource}Data | null>(null);
776
+
777
+ // Form state - populated from API
778
+ {form_state}
779
+
780
+ // Fetch data on mount
781
+ useEffect(() => {{
782
+ async function fetchData() {{
783
+ try {{
784
+ const response = await fetch(`/api/{resource}s/${{id}}`);
785
+ if (!response.ok) {{
786
+ if (response.status === 404) {{
787
+ router.push("/{resource}s");
788
+ return;
789
+ }}
790
+ throw new Error("Failed to fetch {resource}");
791
+ }}
792
+ const data = await response.json();
793
+ set{Resource}(data);
794
+ // Populate form fields
795
+ {populate_fields}
796
+ setLoading(false);
797
+ }} catch (err) {{
798
+ setError(err instanceof Error ? err.message : "An error occurred");
799
+ setLoading(false);
800
+ }}
801
+ }}
802
+ fetchData();
803
+ }}, [id, router]);
804
+
805
+ const handleSave = async (e: React.FormEvent) => {{
806
+ e.preventDefault();
807
+ setSaving(true);
808
+ setError(null);
809
+
810
+ try {{
811
+ const response = await fetch(`/api/{resource}s/${{id}}`, {{
812
+ method: "PATCH",
813
+ headers: {{ "Content-Type": "application/json" }},
814
+ body: JSON.stringify({{
815
+ {save_body}
816
+ }}),
817
+ }});
818
+
819
+ if (!response.ok) {{
820
+ const data = await response.json();
821
+ throw new Error(data.error || "Failed to update {resource}");
822
+ }}
823
+
824
+ router.push("/{resource}s");
825
+ router.refresh();
826
+ }} catch (err) {{
827
+ setError(err instanceof Error ? err.message : "An error occurred");
828
+ setSaving(false);
829
+ }}
830
+ }};
831
+
832
+ const handleDelete = async () => {{
833
+ if (!confirm("Are you sure you want to delete this {resource}?")) {{
834
+ return;
835
+ }}
836
+
837
+ setDeleting(true);
838
+ setError(null);
839
+
840
+ try {{
841
+ const response = await fetch(`/api/{resource}s/${{id}}`, {{
842
+ method: "DELETE"
843
+ }});
844
+
845
+ if (!response.ok) {{
846
+ throw new Error("Failed to delete {resource}");
847
+ }}
848
+
849
+ router.push("/{resource}s");
850
+ router.refresh();
851
+ }} catch (err) {{
852
+ setError(err instanceof Error ? err.message : "An error occurred");
853
+ setDeleting(false);
854
+ }}
855
+ }};
856
+
857
+ if (loading) {{
858
+ return (
859
+ <div className="min-h-screen flex items-center justify-center">
860
+ <div className="text-slate-400">Loading...</div>
861
+ </div>
862
+ );
863
+ }}
864
+
865
+ if (!{resource}) {{
866
+ return (
867
+ <div className="min-h-screen flex items-center justify-center">
868
+ <div className="text-slate-400">{Resource} not found</div>
869
+ </div>
870
+ );
871
+ }}
872
+
873
+ return (
874
+ <div className="min-h-screen">
875
+ <div className="container mx-auto px-4 py-12 max-w-2xl">
876
+ <div className="mb-8">
877
+ <Link
878
+ href="/{resource}s"
879
+ className="link-back group"
880
+ >
881
+ <svg className="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
882
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M15 19l-7-7 7-7" />
883
+ </svg>
884
+ Back to {Resource}s
885
+ </Link>
886
+ </div>
887
+
888
+ <div className="glass-card p-8">
889
+ <h1 className="page-title mb-8">
890
+ Edit {Resource}
891
+ </h1>
892
+
893
+ {{error && (
894
+ <div className="mb-6 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400">
895
+ {{error}}
896
+ </div>
897
+ )}}
898
+
899
+ <form onSubmit={{handleSave}}>
900
+ {form_fields}
901
+
902
+ <div className="flex gap-4 mt-8">
903
+ <button
904
+ type="submit"
905
+ disabled={{saving}}
906
+ className="btn-primary flex-1"
907
+ >
908
+ {{saving ? "Saving..." : "Save Changes"}}
909
+ </button>
910
+ <button
911
+ type="button"
912
+ onClick={{handleDelete}}
913
+ disabled={{deleting}}
914
+ className="btn-danger"
915
+ >
916
+ {{deleting ? "Deleting..." : "Delete"}}
917
+ </button>
918
+ </div>
919
+ </form>
920
+
921
+ <div className="mt-8 pt-6 border-t border-slate-700/50 text-sm text-slate-500">
922
+ <p><strong className="text-slate-400">Created:</strong> {{new Date({resource}.createdAt).toLocaleString()}}</p>
923
+ <p className="mt-1"><strong className="text-slate-400">Updated:</strong> {{new Date({resource}.updatedAt).toLocaleString()}}</p>
924
+ </div>
925
+ </div>
926
+ </div>
927
+ </div>
928
+ );
929
+ }}"""
930
+
931
+ CLIENT_COMPONENT_ACTIONS = """"use client";
932
+
933
+ import {{ useRouter }} from "next/navigation";
934
+ import {{ useState }} from "react";
935
+ import {{ {Resource}Form }} from "@/components/{Resource}Form";
936
+ import type {{ {Resource} }} from "@prisma/client";
937
+
938
+ interface {Resource}ActionsProps {{
939
+ {resource}Id: number;
940
+ {resource}Data?: {Resource};
941
+ }}
942
+
943
+ export function {Resource}Actions({{ {resource}Id, {resource}Data }}: {Resource}ActionsProps) {{
944
+ const router = useRouter();
945
+ const [isEditing, setIsEditing] = useState(false);
946
+ const [deleting, setDeleting] = useState(false);
947
+ const [error, setError] = useState<string | null>(null);
948
+
949
+ const handleDelete = async () => {{
950
+ if (!confirm("Are you sure you want to delete this {resource}?")) {{
951
+ return;
952
+ }}
953
+
954
+ setDeleting(true);
955
+ setError(null);
956
+
957
+ try {{
958
+ const response = await fetch(`/api/{resource}s/${{{resource}Id}}`, {{
959
+ method: "DELETE"
960
+ }});
961
+
962
+ if (!response.ok) {{
963
+ throw new Error("Failed to delete {resource}");
964
+ }}
965
+
966
+ router.push("/{resource}s");
967
+ router.refresh();
968
+ }} catch (err) {{
969
+ setError(err instanceof Error ? err.message : "An error occurred");
970
+ setDeleting(false);
971
+ }}
972
+ }};
973
+
974
+ if (isEditing && {resource}Data) {{
975
+ return (
976
+ <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
977
+ <div className="bg-slate-800 border border-slate-700 rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
978
+ <div className="p-6 border-b border-slate-700">
979
+ <div className="flex justify-between items-center">
980
+ <h2 className="text-xl font-bold text-slate-100">Edit {Resource}</h2>
981
+ <button
982
+ onClick={{() => setIsEditing(false)}}
983
+ className="text-slate-400 hover:text-slate-200 transition-colors"
984
+ >
985
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
986
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M6 18L18 6M6 6l12 12" />
987
+ </svg>
988
+ </button>
989
+ </div>
990
+ </div>
991
+ <div className="p-6">
992
+ <{Resource}Form initialData={{{resource}Data}} mode="edit" />
993
+ </div>
994
+ </div>
995
+ </div>
996
+ );
997
+ }}
998
+
999
+ return (
1000
+ <div className="flex items-center gap-3">
1001
+ {{error && (
1002
+ <div className="absolute top-full right-0 mt-2 bg-red-500/10 border border-red-500/20 text-red-400 p-3 rounded-xl text-sm whitespace-nowrap">
1003
+ {{error}}
1004
+ </div>
1005
+ )}}
1006
+ <button
1007
+ onClick={{() => setIsEditing(true)}}
1008
+ className="inline-flex items-center gap-2 bg-slate-700/50 text-slate-200 px-4 py-2 rounded-xl font-medium border border-slate-600/50 hover:bg-slate-700 transition-all duration-300"
1009
+ >
1010
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1011
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
1012
+ </svg>
1013
+ Edit
1014
+ </button>
1015
+ <button
1016
+ onClick={{handleDelete}}
1017
+ disabled={{deleting}}
1018
+ className="inline-flex items-center gap-2 bg-gradient-to-r from-red-500 to-rose-500 text-white px-4 py-2 rounded-xl font-medium hover:from-red-600 hover:to-rose-600 transition-all duration-300 hover:shadow-lg hover:shadow-red-500/25 disabled:opacity-50"
1019
+ >
1020
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1021
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{2}} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
1022
+ </svg>
1023
+ {{deleting ? "Deleting..." : "Delete"}}
1024
+ </button>
1025
+ </div>
1026
+ );
1027
+ }}"""
1028
+
1029
+ CLIENT_COMPONENT_DETAIL_PAGE = """"use client";
1030
+
1031
+ import {{ useState, useEffect }} from "react";
1032
+ import {{ useRouter }} from "next/navigation";
1033
+ import {{ {Resource}Form }} from "@/components/{Resource}Form";
1034
+ import Link from "next/link";
1035
+
1036
+ interface {Resource} {{
1037
+ id: number;
1038
+ {type_fields}
1039
+ createdAt: Date;
1040
+ updatedAt: Date;
1041
+ }}
1042
+
1043
+ export default function {Resource}DetailPage({{ params }}: {{ params: {{ id: string }} }}) {{
1044
+ const router = useRouter();
1045
+ const [{resource}, set{Resource}] = useState<{Resource} | null>(null);
1046
+ const [error, setError] = useState<string | null>(null);
1047
+ const [loading, setLoading] = useState(true);
1048
+ const [deleting, setDeleting] = useState(false);
1049
+
1050
+ useEffect(() => {{
1051
+ fetch{Resource}();
1052
+ }}, [params.id]);
1053
+
1054
+ const fetch{Resource} = async () => {{
1055
+ try {{
1056
+ const response = await fetch(`/api/{resource}s/${{params.id}}`);
1057
+ if (!response.ok) {{
1058
+ throw new Error("Failed to fetch {resource}");
1059
+ }}
1060
+ const data = await response.json();
1061
+ set{Resource}(data);
1062
+ }} catch (err) {{
1063
+ setError(err instanceof Error ? err.message : "An error occurred");
1064
+ }} finally {{
1065
+ setLoading(false);
1066
+ }}
1067
+ }};
1068
+
1069
+ const handleDelete = async () => {{
1070
+ if (!confirm("Are you sure you want to delete this {resource}?")) return;
1071
+
1072
+ setDeleting(true);
1073
+ try {{
1074
+ const response = await fetch(`/api/{resource}s/${{params.id}}`, {{
1075
+ method: "DELETE"
1076
+ }});
1077
+
1078
+ if (!response.ok) {{
1079
+ throw new Error("Failed to delete {resource}");
1080
+ }}
1081
+
1082
+ router.push("/{resource}s");
1083
+ router.refresh();
1084
+ }} catch (err) {{
1085
+ setError(err instanceof Error ? err.message : "An error occurred");
1086
+ setDeleting(false);
1087
+ }}
1088
+ }};
1089
+
1090
+ if (loading) {{
1091
+ return (
1092
+ <div className="container mx-auto p-8 max-w-2xl">
1093
+ <div className="text-center py-12">Loading...</div>
1094
+ </div>
1095
+ );
1096
+ }}
1097
+
1098
+ if (error || !{resource}) {{
1099
+ return (
1100
+ <div className="container mx-auto p-8 max-w-2xl">
1101
+ <div className="text-center py-12">
1102
+ <p className="text-red-500 mb-4">{{error || "{Resource} not found"}}</p>
1103
+ <Link href="/{resource}s" className="text-blue-500 hover:underline">
1104
+ Back to {Resource}s
1105
+ </Link>
1106
+ </div>
1107
+ </div>
1108
+ );
1109
+ }}
1110
+
1111
+ return (
1112
+ <div className="container mx-auto p-8 max-w-2xl">
1113
+ <div className="mb-6">
1114
+ <Link href="/{resource}s" className="text-blue-500 hover:underline">
1115
+ ← Back to {Resource}s
1116
+ </Link>
1117
+ </div>
1118
+
1119
+ <h1 className="text-3xl font-bold mb-6">Edit {Resource}</h1>
1120
+
1121
+ <{Resource}Form initialData={{{resource}}} mode="edit" />
1122
+
1123
+ <div className="mt-6 pt-6 border-t border-gray-200">
1124
+ <button
1125
+ onClick={{handleDelete}}
1126
+ disabled={{deleting}}
1127
+ className="bg-red-500 text-white px-6 py-2 rounded-md hover:bg-red-600 disabled:opacity-50"
1128
+ >
1129
+ {{deleting ? "Deleting..." : "Delete {Resource}"}}
1130
+ </button>
1131
+ </div>
1132
+
1133
+ <div className="mt-6 bg-gray-50 rounded-lg p-4 text-sm text-gray-600">
1134
+ <p><strong>Created:</strong> {{new Date({resource}.createdAt).toLocaleString()}}</p>
1135
+ <p><strong>Updated:</strong> {{new Date({resource}.updatedAt).toLocaleString()}}</p>
1136
+ </div>
1137
+ </div>
1138
+ );
1139
+ }}"""
1140
+
1141
+
1142
+ def generate_form_field(field_name: str, field_type: str) -> str:
1143
+ """Generate a form field based on type with modern styling."""
1144
+ input_type = {
1145
+ "string": "text",
1146
+ "text": "textarea",
1147
+ "number": "number",
1148
+ "email": "email",
1149
+ "url": "url",
1150
+ "boolean": "checkbox",
1151
+ "date": "date",
1152
+ }.get(field_type.lower(), "text")
1153
+
1154
+ label = field_name.replace("_", " ").title()
1155
+
1156
+ if input_type == "textarea":
1157
+ return f""" <div>
1158
+ <label htmlFor="{field_name}" className="block text-sm font-medium text-slate-300 mb-2">
1159
+ {label}
1160
+ </label>
1161
+ <textarea
1162
+ id="{field_name}"
1163
+ name="{field_name}"
1164
+ value={{formData.{field_name}}}
1165
+ onChange={{handleChange}}
1166
+ rows={{4}}
1167
+ className="w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all duration-300"
1168
+ required
1169
+ />
1170
+ </div>"""
1171
+ elif input_type == "checkbox":
1172
+ return f""" <div className="flex items-center gap-3 p-4 bg-slate-900/30 rounded-xl border border-slate-700/30">
1173
+ <input
1174
+ type="checkbox"
1175
+ id="{field_name}"
1176
+ name="{field_name}"
1177
+ checked={{formData.{field_name}}}
1178
+ onChange={{handleChange}}
1179
+ className="w-5 h-5 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent focus:ring-2 focus:ring-indigo-500/50 cursor-pointer transition-all duration-300"
1180
+ />
1181
+ <label htmlFor="{field_name}" className="text-slate-300 cursor-pointer">
1182
+ {label}
1183
+ </label>
1184
+ </div>"""
1185
+ else:
1186
+ return f""" <div>
1187
+ <label htmlFor="{field_name}" className="block text-sm font-medium text-slate-300 mb-2">
1188
+ {label}
1189
+ </label>
1190
+ <input
1191
+ type="{input_type}"
1192
+ id="{field_name}"
1193
+ name="{field_name}"
1194
+ value={{formData.{field_name}}}
1195
+ onChange={{handleChange}}
1196
+ className="w-full px-4 py-3 bg-slate-900/50 border border-slate-700/50 rounded-xl text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all duration-300"
1197
+ required
1198
+ />
1199
+ </div>"""
1200
+
1201
+
1202
+ # ========== Import Generation ==========
1203
+
1204
+
1205
+ def generate_api_imports(_operations: list, uses_validation: bool = True) -> str:
1206
+ """Generate appropriate imports for API routes."""
1207
+ imports = [
1208
+ 'import { NextResponse } from "next/server";',
1209
+ 'import { prisma } from "@/lib/prisma";',
1210
+ ]
1211
+
1212
+ if uses_validation:
1213
+ imports.append('import { z } from "zod";')
1214
+
1215
+ return "\n".join(imports)
1216
+
1217
+
1218
+ def generate_component_imports(component_type: str, uses_data: bool = False) -> str:
1219
+ """Generate appropriate imports for React components."""
1220
+ imports = []
1221
+
1222
+ if component_type == "client":
1223
+ imports.extend(
1224
+ [
1225
+ '"use client";',
1226
+ "",
1227
+ 'import { useState } from "react";',
1228
+ 'import { useRouter } from "next/navigation";',
1229
+ ]
1230
+ )
1231
+ elif uses_data:
1232
+ imports.append('import { prisma } from "@/lib/prisma";')
1233
+
1234
+ imports.append('import Link from "next/link";')
1235
+
1236
+ return "\n".join(imports)
1237
+
1238
+
1239
+ # ========== Helper Functions ==========
1240
+
1241
+
1242
+ def pluralize(word: str) -> str:
1243
+ """Simple pluralization (can be enhanced)."""
1244
+ if word.endswith("y"):
1245
+ return word[:-1] + "ies"
1246
+ elif word.endswith(("s", "x", "z", "ch", "sh")):
1247
+ return word + "es"
1248
+ else:
1249
+ return word + "s"
1250
+
1251
+
1252
+ def generate_field_display(fields: dict, max_fields: int = 3) -> str:
1253
+ """Generate JSX for displaying resource fields with type-aware rendering.
1254
+
1255
+ Boolean fields render as checkboxes with strikethrough styling for completed items.
1256
+ String fields render as text with proper hierarchy. Uses modern dark theme styling.
1257
+
1258
+ Args:
1259
+ fields: Dictionary mapping field names to their types (e.g., {"title": "string", "completed": "boolean"})
1260
+ max_fields: Maximum number of fields to display (default: 3)
1261
+
1262
+ Returns:
1263
+ JSX string for field display
1264
+ """
1265
+ display_fields = []
1266
+ title_field = None
1267
+ boolean_field = None
1268
+
1269
+ # Find primary title field and boolean field
1270
+ for field_name, field_type in fields.items():
1271
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
1272
+ continue
1273
+ if field_name.lower() in {"title", "name"} and not title_field:
1274
+ title_field = field_name
1275
+ if field_type.lower() == "boolean" and not boolean_field:
1276
+ boolean_field = field_name
1277
+
1278
+ # Generate checkbox + title combo for boolean fields (e.g., completed todo)
1279
+ if boolean_field and title_field:
1280
+ # Render checkbox with title that has strikethrough when boolean is true
1281
+ display_fields.append(f"""<div className="flex items-center gap-4">
1282
+ <div className="relative">
1283
+ <input
1284
+ type="checkbox"
1285
+ checked={{item.{boolean_field}}}
1286
+ readOnly
1287
+ className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-pointer transition-all duration-300"
1288
+ />
1289
+ {{item.{boolean_field} && (
1290
+ <svg className="absolute inset-0 w-6 h-6 text-white pointer-events-none p-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1291
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{3}} d="M5 13l4 4L19 7" />
1292
+ </svg>
1293
+ )}}
1294
+ </div>
1295
+ <h3 className={{`font-semibold text-lg ${{item.{boolean_field} ? "line-through text-slate-500" : "text-slate-100"}}`}}>
1296
+ {{item.{title_field}}}
1297
+ </h3>
1298
+ </div>""")
1299
+ elif title_field:
1300
+ # Just render title without checkbox
1301
+ display_fields.append(
1302
+ f'<h3 className="font-semibold text-lg text-slate-100">{{item.{title_field}}}</h3>'
1303
+ )
1304
+
1305
+ # Add remaining non-boolean, non-title fields as secondary text
1306
+ for field_name, field_type in list(fields.items())[:max_fields]:
1307
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
1308
+ continue
1309
+ if field_name == title_field or field_name == boolean_field:
1310
+ continue
1311
+ if field_type.lower() != "boolean":
1312
+ display_fields.append(
1313
+ f'<p className="text-slate-400 text-sm mt-1">{{item.{field_name}}}</p>'
1314
+ )
1315
+
1316
+ return (
1317
+ "\n ".join(display_fields)
1318
+ if display_fields
1319
+ else '<p className="text-slate-400">{{item.id}}</p>'
1320
+ )
1321
+
1322
+
1323
+ def generate_new_page(resource_name: str) -> str:
1324
+ """Generate a 'new' page component that uses the form component.
1325
+
1326
+ Args:
1327
+ resource_name: Name of the resource (e.g., "todo", "product")
1328
+
1329
+ Returns:
1330
+ Complete TypeScript/React page component code
1331
+ """
1332
+ resource = resource_name.lower()
1333
+ Resource = resource_name.capitalize()
1334
+
1335
+ return CLIENT_COMPONENT_NEW_PAGE.format(resource=resource, Resource=Resource)
1336
+
1337
+
1338
+ def generate_detail_page(resource_name: str, fields: dict) -> str:
1339
+ """Generate an edit page with pre-populated form fields.
1340
+
1341
+ Args:
1342
+ resource_name: Name of the resource (e.g., "todo", "product")
1343
+ fields: Dictionary of field names to types
1344
+
1345
+ Returns:
1346
+ Complete TypeScript/React page component code
1347
+ """
1348
+ resource = resource_name.lower()
1349
+ Resource = resource_name.capitalize()
1350
+
1351
+ # Generate TypeScript interface fields
1352
+ interface_lines = []
1353
+ form_state_lines = []
1354
+ populate_lines = []
1355
+ save_body_lines = []
1356
+ form_field_lines = []
1357
+
1358
+ for field_name, field_type in fields.items():
1359
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
1360
+ continue
1361
+
1362
+ # TypeScript types
1363
+ ts_type = _get_typescript_type(field_type)
1364
+ interface_lines.append(f" {field_name}: {ts_type};")
1365
+
1366
+ # useState declarations
1367
+ default_val = _get_default_value(field_type)
1368
+ form_state_lines.append(
1369
+ f" const [{field_name}, set{field_name.capitalize()}] = useState<{ts_type}>({default_val});"
1370
+ )
1371
+
1372
+ # Populate from API response
1373
+ populate_lines.append(
1374
+ f" set{field_name.capitalize()}(data.{field_name});"
1375
+ )
1376
+
1377
+ # Save body
1378
+ save_body_lines.append(f" {field_name},")
1379
+
1380
+ # Form field JSX
1381
+ label = field_name.replace("_", " ").title()
1382
+ form_field_lines.append(
1383
+ _generate_edit_form_field(field_name, field_type, label)
1384
+ )
1385
+
1386
+ return SERVER_COMPONENT_DETAIL.format(
1387
+ resource=resource,
1388
+ Resource=Resource,
1389
+ interface_fields="\n".join(interface_lines),
1390
+ form_state="\n".join(form_state_lines),
1391
+ populate_fields="\n".join(populate_lines),
1392
+ save_body="\n".join(save_body_lines),
1393
+ form_fields="\n\n".join(form_field_lines),
1394
+ )
1395
+
1396
+
1397
+ def _get_typescript_type(field_type: str) -> str:
1398
+ """Convert field type to TypeScript type."""
1399
+ type_lower = field_type.lower()
1400
+ if type_lower == "boolean":
1401
+ return "boolean"
1402
+ if type_lower in ["number", "int", "integer", "float"]:
1403
+ return "number"
1404
+ return "string"
1405
+
1406
+
1407
+ def _get_default_value(field_type: str) -> str:
1408
+ """Get default value for useState based on field type."""
1409
+ type_lower = field_type.lower()
1410
+ if type_lower == "boolean":
1411
+ return "false"
1412
+ if type_lower in ["number", "int", "integer", "float"]:
1413
+ return "0"
1414
+ return '""'
1415
+
1416
+
1417
+ def _generate_edit_form_field(field_name: str, field_type: str, label: str) -> str:
1418
+ """Generate a single form field for editing."""
1419
+ type_lower = field_type.lower()
1420
+
1421
+ if type_lower == "boolean":
1422
+ return f""" <div className="mb-6">
1423
+ <label className="flex items-center gap-3 cursor-pointer">
1424
+ <input
1425
+ type="checkbox"
1426
+ checked={{{field_name}}}
1427
+ onChange={{(e) => set{field_name.capitalize()}(e.target.checked)}}
1428
+ className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-pointer transition-all duration-300"
1429
+ />
1430
+ <span className="text-slate-200 font-medium">{label}</span>
1431
+ </label>
1432
+ </div>"""
1433
+ elif type_lower in ["number", "int", "integer", "float"]:
1434
+ return f""" <div className="mb-6">
1435
+ <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>
1436
+ <input
1437
+ type="number"
1438
+ value={{{field_name}}}
1439
+ onChange={{(e) => set{field_name.capitalize()}(parseFloat(e.target.value) || 0)}}
1440
+ className="input-field"
1441
+ />
1442
+ </div>"""
1443
+ else:
1444
+ # String/text fields
1445
+ return f""" <div className="mb-6">
1446
+ <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>
1447
+ <input
1448
+ type="text"
1449
+ value={{{field_name}}}
1450
+ onChange={{(e) => set{field_name.capitalize()}(e.target.value)}}
1451
+ className="input-field"
1452
+ required
1453
+ />
1454
+ </div>"""
1455
+
1456
+
1457
+ def generate_actions_component(resource_name: str) -> str:
1458
+ """Generate the actions component for delete functionality.
1459
+
1460
+ Args:
1461
+ resource_name: Name of the resource (e.g., "todo", "product")
1462
+
1463
+ Returns:
1464
+ Complete TypeScript/React client component code
1465
+ """
1466
+ resource = resource_name.lower()
1467
+ Resource = resource_name.capitalize()
1468
+
1469
+ return CLIENT_COMPONENT_ACTIONS.format(resource=resource, Resource=Resource)
1470
+
1471
+
1472
+ def _generate_detail_field_display(resource: str, fields: dict) -> str:
1473
+ """Generate JSX for displaying resource fields in detail view.
1474
+
1475
+ Boolean fields render as visual checkboxes with strikethrough for completed items.
1476
+ Uses modern dark theme styling.
1477
+
1478
+ Args:
1479
+ resource: Resource variable name (e.g., "todo")
1480
+ fields: Dictionary mapping field names to their types
1481
+
1482
+ Returns:
1483
+ JSX string for detail field display
1484
+ """
1485
+ display_fields = []
1486
+
1487
+ # Find title and boolean fields for special combined rendering
1488
+ title_field = None
1489
+ boolean_field = None
1490
+ for field_name, field_type in fields.items():
1491
+ if field_name.lower() in {"title", "name"} and not title_field:
1492
+ title_field = field_name
1493
+ if field_type.lower() == "boolean" and not boolean_field:
1494
+ boolean_field = field_name
1495
+
1496
+ for field_name, field_type in fields.items():
1497
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
1498
+ continue
1499
+
1500
+ label = field_name.replace("_", " ").title()
1501
+
1502
+ if field_type.lower() == "boolean":
1503
+ # Render checkbox with visual feedback
1504
+ display_fields.append(
1505
+ f' <div className="mb-6 p-4 bg-slate-900/30 rounded-xl border border-slate-700/30">\n'
1506
+ f' <label className="block text-sm font-medium text-slate-400 mb-3">{label}</label>\n'
1507
+ f' <div className="flex items-center gap-3">\n'
1508
+ f' <div className="relative">\n'
1509
+ f" <input\n"
1510
+ f' type="checkbox"\n'
1511
+ f" checked={{{resource}.{field_name}}}\n"
1512
+ f" readOnly\n"
1513
+ f' className="w-6 h-6 rounded-lg border-2 border-slate-600 bg-slate-800/50 checked:bg-gradient-to-r checked:from-indigo-500 checked:to-purple-500 checked:border-transparent appearance-none cursor-default transition-all duration-300"\n'
1514
+ f" />\n"
1515
+ f" {{{resource}.{field_name} && (\n"
1516
+ f' <svg className="absolute inset-0 w-6 h-6 text-white pointer-events-none p-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">\n'
1517
+ f' <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={{3}} d="M5 13l4 4L19 7" />\n'
1518
+ f" </svg>\n"
1519
+ f" )}}\n"
1520
+ f" </div>\n"
1521
+ f' <span className={{{resource}.{field_name} ? "text-emerald-400 font-medium" : "text-slate-500"}}>\n'
1522
+ f' {{{resource}.{field_name} ? "Yes" : "No"}}\n'
1523
+ f" </span>\n"
1524
+ f" </div>\n"
1525
+ f" </div>"
1526
+ )
1527
+ elif field_type.lower() in ["date", "datetime", "timestamp"]:
1528
+ display_fields.append(
1529
+ f' <div className="mb-6">\n'
1530
+ f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1531
+ f' <p className="text-lg text-slate-200">{{new Date({resource}.{field_name}).toLocaleDateString()}}</p>\n'
1532
+ f" </div>"
1533
+ )
1534
+ else:
1535
+ # For title field with boolean, add strikethrough styling
1536
+ if field_name == title_field and boolean_field:
1537
+ class_expr = f'{{{resource}.{boolean_field} ? "text-xl text-slate-500 line-through" : "text-xl text-slate-100"}}'
1538
+ display_fields.append(
1539
+ f' <div className="mb-6">\n'
1540
+ f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1541
+ f" <p className={class_expr}>{{{resource}.{field_name}}}</p>\n"
1542
+ f" </div>"
1543
+ )
1544
+ else:
1545
+ display_fields.append(
1546
+ f' <div className="mb-6">\n'
1547
+ f' <label className="block text-sm font-medium text-slate-400 mb-2">{label}</label>\n'
1548
+ f' <p className="text-xl text-slate-100">{{{resource}.{field_name}}}</p>\n'
1549
+ f" </div>"
1550
+ )
1551
+
1552
+ return (
1553
+ "\n".join(display_fields)
1554
+ if display_fields
1555
+ else ' <p className="text-slate-400">No fields to display</p>'
1556
+ )
1557
+
1558
+
1559
+ def _map_type_to_typescript(field_type: str) -> str:
1560
+ """Map field type to TypeScript type."""
1561
+ type_mapping = {
1562
+ "string": "string",
1563
+ "text": "string",
1564
+ "number": "number",
1565
+ "float": "number",
1566
+ "boolean": "boolean",
1567
+ "date": "Date",
1568
+ "datetime": "Date",
1569
+ "timestamp": "Date",
1570
+ "email": "string",
1571
+ "url": "string",
1572
+ }
1573
+ return type_mapping.get(field_type.lower(), "string")
1574
+
1575
+
1576
+ # ========== Test Templates (Vitest) ==========
1577
+
1578
+ VITEST_CONFIG = """import { defineConfig } from 'vitest/config';
1579
+ import react from '@vitejs/plugin-react';
1580
+ import path from 'path';
1581
+
1582
+ export default defineConfig({
1583
+ plugins: [react()],
1584
+ test: {
1585
+ environment: 'jsdom',
1586
+ globals: true,
1587
+ setupFiles: ['./tests/setup.ts'],
1588
+ include: ['**/__tests__/**/*.test.{ts,tsx}'],
1589
+ coverage: {
1590
+ provider: 'v8',
1591
+ reporter: ['text', 'json', 'html'],
1592
+ },
1593
+ },
1594
+ resolve: {
1595
+ alias: {
1596
+ '@': path.resolve(__dirname, './src'),
1597
+ },
1598
+ },
1599
+ });
1600
+ """
1601
+
1602
+ TEST_SETUP = """import '@testing-library/jest-dom';
1603
+ import {{ vi }} from 'vitest';
1604
+
1605
+ // Mock next/navigation
1606
+ vi.mock('next/navigation', () => ({{
1607
+ useRouter: () => ({{
1608
+ push: vi.fn(),
1609
+ back: vi.fn(),
1610
+ refresh: vi.fn(),
1611
+ replace: vi.fn(),
1612
+ }}),
1613
+ usePathname: () => '/',
1614
+ useSearchParams: () => new URLSearchParams(),
1615
+ }}));
1616
+
1617
+ // Mock Prisma client
1618
+ vi.mock('@/lib/prisma', () => ({{
1619
+ prisma: {{
1620
+ {resource}: {{
1621
+ findMany: vi.fn(),
1622
+ findUnique: vi.fn(),
1623
+ create: vi.fn(),
1624
+ update: vi.fn(),
1625
+ delete: vi.fn(),
1626
+ count: vi.fn(),
1627
+ }},
1628
+ }},
1629
+ }}));
1630
+ """
1631
+
1632
+
1633
+ COMPONENT_TEST_FORM = """import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
1634
+ import {{ render, screen, fireEvent, waitFor }} from '@testing-library/react';
1635
+ import {{ {Resource}Form }} from '../{Resource}Form';
1636
+
1637
+ // Mock fetch
1638
+ global.fetch = vi.fn();
1639
+
1640
+ describe('{Resource}Form', () => {{
1641
+ beforeEach(() => {{
1642
+ vi.clearAllMocks();
1643
+ (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1644
+ ok: true,
1645
+ json: () => Promise.resolve({{ id: 1 }}),
1646
+ }});
1647
+ }});
1648
+
1649
+ it('renders all form fields', () => {{
1650
+ render(<{Resource}Form />);
1651
+
1652
+ {form_field_assertions}
1653
+ }});
1654
+
1655
+ it('submits form data correctly in create mode', async () => {{
1656
+ render(<{Resource}Form mode="create" />);
1657
+
1658
+ {form_fill_actions}
1659
+
1660
+ fireEvent.click(screen.getByRole('button', {{ name: /create/i }}));
1661
+
1662
+ await waitFor(() => {{
1663
+ expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}', expect.objectContaining({{
1664
+ method: 'POST',
1665
+ headers: {{ 'Content-Type': 'application/json' }},
1666
+ }}));
1667
+ }});
1668
+ }});
1669
+
1670
+ it('submits form data correctly in edit mode', async () => {{
1671
+ const initialData = {{ id: 1, {test_data_fields} }};
1672
+ render(<{Resource}Form initialData={{initialData}} mode="edit" />);
1673
+
1674
+ fireEvent.click(screen.getByRole('button', {{ name: /update/i }}));
1675
+
1676
+ await waitFor(() => {{
1677
+ expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}/1', expect.objectContaining({{
1678
+ method: 'PATCH',
1679
+ }}));
1680
+ }});
1681
+ }});
1682
+
1683
+ it('displays error message on failed submission', async () => {{
1684
+ (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1685
+ ok: false,
1686
+ json: () => Promise.resolve({{ error: 'Validation failed' }}),
1687
+ }});
1688
+
1689
+ render(<{Resource}Form />);
1690
+
1691
+ {form_fill_actions}
1692
+
1693
+ fireEvent.click(screen.getByRole('button', {{ name: /create/i }}));
1694
+
1695
+ await waitFor(() => {{
1696
+ expect(screen.getByText(/validation failed/i)).toBeInTheDocument();
1697
+ }});
1698
+ }});
1699
+ }});
1700
+ """
1701
+
1702
+ COMPONENT_TEST_ACTIONS = """import {{ describe, it, expect, vi, beforeEach }} from 'vitest';
1703
+ import {{ render, screen, fireEvent, waitFor }} from '@testing-library/react';
1704
+ import {{ {Resource}Actions }} from '../{Resource}Actions';
1705
+
1706
+ // Mock fetch and confirm
1707
+ global.fetch = vi.fn();
1708
+ global.confirm = vi.fn();
1709
+
1710
+ describe('{Resource}Actions', () => {{
1711
+ beforeEach(() => {{
1712
+ vi.clearAllMocks();
1713
+ (global.confirm as ReturnType<typeof vi.fn>).mockReturnValue(true);
1714
+ (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1715
+ ok: true,
1716
+ json: () => Promise.resolve({{ success: true }}),
1717
+ }});
1718
+ }});
1719
+
1720
+ it('renders delete button', () => {{
1721
+ render(<{Resource}Actions {resource}Id={{1}} />);
1722
+
1723
+ expect(screen.getByRole('button', {{ name: /delete/i }})).toBeInTheDocument();
1724
+ }});
1725
+
1726
+ it('confirms before deleting', async () => {{
1727
+ render(<{Resource}Actions {resource}Id={{1}} />);
1728
+
1729
+ fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1730
+
1731
+ expect(confirm).toHaveBeenCalled();
1732
+ }});
1733
+
1734
+ it('calls delete API on confirmation', async () => {{
1735
+ render(<{Resource}Actions {resource}Id={{1}} />);
1736
+
1737
+ fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1738
+
1739
+ await waitFor(() => {{
1740
+ expect(fetch).toHaveBeenCalledWith('/api/{resource_plural}/1', {{
1741
+ method: 'DELETE',
1742
+ }});
1743
+ }});
1744
+ }});
1745
+
1746
+ it('does not call API when delete is cancelled', () => {{
1747
+ (global.confirm as ReturnType<typeof vi.fn>).mockReturnValue(false);
1748
+
1749
+ render(<{Resource}Actions {resource}Id={{1}} />);
1750
+
1751
+ fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1752
+
1753
+ expect(fetch).not.toHaveBeenCalled();
1754
+ }});
1755
+
1756
+ it('displays error message on failed deletion', async () => {{
1757
+ (global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({{
1758
+ ok: false,
1759
+ }});
1760
+
1761
+ render(<{Resource}Actions {resource}Id={{1}} />);
1762
+
1763
+ fireEvent.click(screen.getByRole('button', {{ name: /delete/i }}));
1764
+
1765
+ await waitFor(() => {{
1766
+ expect(screen.getByText(/failed to delete/i)).toBeInTheDocument();
1767
+ }});
1768
+ }});
1769
+ }});
1770
+ """
1771
+
1772
+
1773
+ def generate_test_data_fields(fields: dict, variant: int = 1) -> str:
1774
+ """Generate test data fields for test templates.
1775
+
1776
+ Args:
1777
+ fields: Dictionary of field names to types
1778
+ variant: Variant number for different test data
1779
+
1780
+ Returns:
1781
+ String of test data field assignments
1782
+ """
1783
+ test_values = []
1784
+ for field_name, field_type in fields.items():
1785
+ if field_name in ["id", "createdAt", "updatedAt"]:
1786
+ continue
1787
+
1788
+ normalized_type = field_type.lower()
1789
+ if normalized_type == "boolean":
1790
+ value = "true" if variant == 1 else "false"
1791
+ elif normalized_type in ["number", "int", "float"]:
1792
+ value = str(variant * 10)
1793
+ elif normalized_type == "email":
1794
+ value = f'"test{variant}@example.com"'
1795
+ elif normalized_type == "url":
1796
+ value = f'"https://example{variant}.com"'
1797
+ else:
1798
+ value = f'"{field_name.title()} {variant}"'
1799
+
1800
+ test_values.append(f"{field_name}: {value}")
1801
+
1802
+ return ", ".join(test_values)
1803
+
1804
+
1805
+ def generate_form_field_assertions(fields: dict) -> str:
1806
+ """Generate test assertions for form field presence.
1807
+
1808
+ Args:
1809
+ fields: Dictionary of field names to types
1810
+
1811
+ Returns:
1812
+ String of expect statements
1813
+ """
1814
+ assertions = []
1815
+ for field_name, field_type in fields.items():
1816
+ if field_name in ["id", "createdAt", "updatedAt"]:
1817
+ continue
1818
+
1819
+ label = field_name.replace("_", " ").title()
1820
+ if field_type.lower() == "boolean":
1821
+ assertions.append(
1822
+ f"expect(screen.getByLabelText(/{label}/i)).toBeInTheDocument();"
1823
+ )
1824
+ else:
1825
+ assertions.append(
1826
+ f"expect(screen.getByLabelText(/{label}/i)).toBeInTheDocument();"
1827
+ )
1828
+
1829
+ return "\n ".join(assertions)
1830
+
1831
+
1832
+ def generate_form_fill_actions(fields: dict) -> str:
1833
+ """Generate test actions to fill form fields.
1834
+
1835
+ Args:
1836
+ fields: Dictionary of field names to types
1837
+
1838
+ Returns:
1839
+ String of fireEvent calls
1840
+ """
1841
+ actions = []
1842
+ for field_name, field_type in fields.items():
1843
+ if field_name in ["id", "createdAt", "updatedAt"]:
1844
+ continue
1845
+
1846
+ label = field_name.replace("_", " ").title()
1847
+ normalized_type = field_type.lower()
1848
+
1849
+ if normalized_type == "boolean":
1850
+ actions.append(f"fireEvent.click(screen.getByLabelText(/{label}/i));")
1851
+ else:
1852
+ test_value = "Test Value"
1853
+ if normalized_type in ["number", "int", "float"]:
1854
+ test_value = "42"
1855
+ elif normalized_type == "email":
1856
+ test_value = "test@example.com"
1857
+ elif normalized_type == "url":
1858
+ test_value = "https://example.com"
1859
+
1860
+ actions.append(
1861
+ f"fireEvent.change(screen.getByLabelText(/{label}/i), {{ target: {{ value: '{test_value}' }} }});"
1862
+ )
1863
+
1864
+ return "\n ".join(actions)
1865
+
1866
+
1867
+ # ========== Style Test Templates (Issue #1002) ==========
1868
+
1869
+ STYLE_TEST_TEMPLATE = """import { describe, it, expect, beforeAll } from 'vitest';
1870
+ import * as fs from 'fs';
1871
+ import * as path from 'path';
1872
+
1873
+ describe('Global CSS Integrity', () => {
1874
+ const globalsPath = path.join(process.cwd(), 'src/app/globals.css');
1875
+ let cssContent: string;
1876
+
1877
+ beforeAll(() => {
1878
+ cssContent = fs.readFileSync(globalsPath, 'utf-8');
1879
+ });
1880
+
1881
+ describe('File Content Type (CRITICAL - Issue #1002)', () => {
1882
+ it('is valid CSS, not TypeScript/JavaScript', () => {
1883
+ // These patterns indicate wrong file content - always invalid
1884
+ expect(cssContent).not.toMatch(/^\\s*import\\s+.*from/m);
1885
+ expect(cssContent).not.toMatch(/^\\s*export\\s+(default|const|function|class)/m);
1886
+ expect(cssContent).not.toMatch(/"use client"|'use client'/);
1887
+ expect(cssContent).not.toMatch(/^\\s*interface\\s+\\w+/m);
1888
+ expect(cssContent).not.toMatch(/^\\s*type\\s+\\w+\\s*=/m);
1889
+ expect(cssContent).not.toMatch(/<[A-Z][a-zA-Z]*[\\s/>]/); // JSX tags
1890
+ });
1891
+
1892
+ it('has balanced CSS braces', () => {
1893
+ const open = (cssContent.match(/\\{/g) || []).length;
1894
+ const close = (cssContent.match(/\\}/g) || []).length;
1895
+ expect(open).toBe(close);
1896
+ });
1897
+ });
1898
+
1899
+ describe('Tailwind Framework', () => {
1900
+ it('includes Tailwind directives', () => {
1901
+ // At minimum, CSS should have Tailwind setup
1902
+ const hasTailwind =
1903
+ cssContent.includes('@tailwind') ||
1904
+ cssContent.includes('@import "tailwindcss');
1905
+ expect(hasTailwind).toBe(true);
1906
+ });
1907
+ });
1908
+
1909
+ describe('Design System Classes', () => {
1910
+ it('defines glass-card class', () => {
1911
+ expect(cssContent).toContain('.glass-card');
1912
+ });
1913
+
1914
+ it('defines btn-primary class', () => {
1915
+ expect(cssContent).toContain('.btn-primary');
1916
+ });
1917
+
1918
+ it('defines page-title class', () => {
1919
+ expect(cssContent).toContain('.page-title');
1920
+ });
1921
+ });
1922
+ });
1923
+ """
1924
+
1925
+ ROUTES_TEST_TEMPLATE = """import {{ describe, it, expect }} from 'vitest';
1926
+ import * as fs from 'fs';
1927
+ import * as path from 'path';
1928
+ import {{ glob }} from 'glob';
1929
+
1930
+ describe('Next.js App Router Structure', () => {{
1931
+ const appDir = path.join(process.cwd(), 'src/app');
1932
+
1933
+ describe('Root Layout (Global Styles Entry Point)', () => {{
1934
+ it('layout.tsx exists', () => {{
1935
+ const layoutPath = path.join(appDir, 'layout.tsx');
1936
+ expect(fs.existsSync(layoutPath)).toBe(true);
1937
+ }});
1938
+
1939
+ it('layout imports globals.css', () => {{
1940
+ const layoutPath = path.join(appDir, 'layout.tsx');
1941
+ const content = fs.readFileSync(layoutPath, 'utf-8');
1942
+ // Should import globals.css (various import patterns)
1943
+ expect(content).toMatch(/import\\s+['"]\\.\\/globals\\.css['"]|import\\s+['"]@\\/app\\/globals\\.css['"]/);
1944
+ }});
1945
+ }});
1946
+
1947
+ describe('Page Structure', () => {{
1948
+ it('all page.tsx files are valid React components', () => {{
1949
+ const pages = glob.sync('**/page.tsx', {{ cwd: appDir }});
1950
+
1951
+ for (const page of pages) {{
1952
+ const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1953
+
1954
+ // Should have an export (default or named)
1955
+ expect(content).toMatch(/export\\s+(default\\s+)?(async\\s+)?function|export\\s+default/);
1956
+
1957
+ // Should not be empty
1958
+ expect(content.trim().length).toBeGreaterThan(50);
1959
+ }}
1960
+ }});
1961
+
1962
+ it('dynamic routes have params handling', () => {{
1963
+ const dynamicPages = glob.sync('**/\\\\[*\\\\]/**/page.tsx', {{ cwd: appDir }});
1964
+
1965
+ for (const page of dynamicPages) {{
1966
+ const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1967
+ // Should reference params somewhere
1968
+ expect(content).toMatch(/params|searchParams/);
1969
+ }}
1970
+ }});
1971
+ }});
1972
+
1973
+ describe('Styling Consistency', () => {{
1974
+ it('pages use className attributes (styled, not unstyled)', () => {{
1975
+ const pages = glob.sync('**/page.tsx', {{ cwd: appDir, ignore: '**/api/**' }});
1976
+
1977
+ for (const page of pages) {{
1978
+ const content = fs.readFileSync(path.join(appDir, page), 'utf-8');
1979
+ // Each page should have some styling
1980
+ const classNameCount = (content.match(/className=/g) || []).length;
1981
+ expect(classNameCount).toBeGreaterThan(0);
1982
+ }}
1983
+ }});
1984
+ }});
1985
+
1986
+ describe('API Routes Exist', () => {{
1987
+ it('has API routes for CRUD operations', () => {{
1988
+ const apiRoutes = glob.sync('**/route.ts', {{ cwd: path.join(appDir, 'api') }});
1989
+ expect(apiRoutes.length).toBeGreaterThan(0);
1990
+ }});
1991
+
1992
+ it('API routes export HTTP methods', () => {{
1993
+ const apiDir = path.join(appDir, 'api');
1994
+ if (!fs.existsSync(apiDir)) return; // Skip if no API dir
1995
+
1996
+ const apiRoutes = glob.sync('**/route.ts', {{ cwd: apiDir }});
1997
+
1998
+ for (const route of apiRoutes) {{
1999
+ const content = fs.readFileSync(path.join(apiDir, route), 'utf-8');
2000
+ // Should export at least one HTTP method
2001
+ expect(content).toMatch(/export\\s+(async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE)/);
2002
+ }}
2003
+ }});
2004
+ }});
2005
+ }});
2006
+ """
2007
+
2008
+
2009
+ def generate_style_test_content(_resource_name: str = "Item") -> str:
2010
+ """Generate the content for styles.test.ts.
2011
+
2012
+ Args:
2013
+ _resource_name: Resource name for component checks (unused, kept for API compatibility)
2014
+
2015
+ Returns:
2016
+ Complete test file content
2017
+ """
2018
+ return STYLE_TEST_TEMPLATE
2019
+
2020
+
2021
+ def generate_routes_test_content(resource_name: str = "Item") -> str:
2022
+ """Generate the content for routes.test.ts.
2023
+
2024
+ Args:
2025
+ resource_name: Resource name for route checks
2026
+
2027
+ Returns:
2028
+ Complete test file content
2029
+ """
2030
+ return ROUTES_TEST_TEMPLATE.format(
2031
+ resource=resource_name.lower(),
2032
+ Resource=resource_name.capitalize(),
2033
+ resource_plural=pluralize(resource_name.lower()),
2034
+ )