amd-gaia 0.14.3__py3-none-any.whl → 0.15.1__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 (181) hide show
  1. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5621
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.14.3.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -729
  180. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,2036 +1,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(
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(
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
+ )