swallowkit 1.0.0-beta.2 → 1.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.ja.md +312 -215
- package/README.md +369 -216
- package/dist/__tests__/fixtures.d.ts +22 -0
- package/dist/__tests__/fixtures.d.ts.map +1 -0
- package/dist/__tests__/fixtures.js +146 -0
- package/dist/__tests__/fixtures.js.map +1 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +444 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/add-connector.d.ts +20 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -0
- package/dist/cli/commands/add-connector.js +163 -0
- package/dist/cli/commands/add-connector.js.map +1 -0
- package/dist/cli/commands/create-model.d.ts +1 -4
- package/dist/cli/commands/create-model.d.ts.map +1 -1
- package/dist/cli/commands/create-model.js +21 -82
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/dev-seeds.d.ts +35 -0
- package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
- package/dist/cli/commands/dev-seeds.js +292 -0
- package/dist/cli/commands/dev-seeds.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +19 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +476 -117
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +13 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +2627 -1708
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +3 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +617 -129
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +162 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +8 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +90 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +101 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
- package/dist/core/mock/connector-mock-server.js +480 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -0
- package/dist/core/mock/zod-mock-generator.d.ts +14 -0
- package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
- package/dist/core/mock/zod-mock-generator.js +163 -0
- package/dist/core/mock/zod-mock-generator.js.map +1 -0
- package/dist/core/operations/create-model.d.ts +15 -0
- package/dist/core/operations/create-model.d.ts.map +1 -0
- package/dist/core/operations/create-model.js +171 -0
- package/dist/core/operations/create-model.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +32 -0
- package/dist/core/operations/runtime.d.ts.map +1 -0
- package/dist/core/operations/runtime.js +225 -0
- package/dist/core/operations/runtime.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts +16 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
- package/dist/core/operations/scaffold-machine.js +63 -0
- package/dist/core/operations/scaffold-machine.js.map +1 -0
- package/dist/core/project/manifest.d.ts +92 -0
- package/dist/core/project/manifest.d.ts.map +1 -0
- package/dist/core/project/manifest.js +321 -0
- package/dist/core/project/manifest.js.map +1 -0
- package/dist/core/project/validation.d.ts +20 -0
- package/dist/core/project/validation.d.ts.map +1 -0
- package/dist/core/project/validation.js +204 -0
- package/dist/core/project/validation.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1244 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.js +1027 -0
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
- package/dist/core/scaffold/functions-generator.d.ts +7 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +920 -213
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +20 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +329 -135
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +314 -182
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +3 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
- package/dist/core/scaffold/openapi-generator.js +190 -0
- package/dist/core/scaffold/openapi-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +768 -663
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/database/base-model.d.ts +3 -3
- package/dist/database/base-model.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +16 -0
- package/dist/machine/contracts.d.ts.map +1 -0
- package/dist/machine/contracts.js +3 -0
- package/dist/machine/contracts.js.map +1 -0
- package/dist/machine/errors.d.ts +11 -0
- package/dist/machine/errors.d.ts.map +1 -0
- package/dist/machine/errors.js +34 -0
- package/dist/machine/errors.js.map +1 -0
- package/dist/machine/index.d.ts +3 -0
- package/dist/machine/index.d.ts.map +1 -0
- package/dist/machine/index.js +156 -0
- package/dist/machine/index.js.map +1 -0
- package/dist/mcp/index.d.ts +25 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +184 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +109 -0
- package/dist/utils/package-manager.d.ts.map +1 -0
- package/dist/utils/package-manager.js +215 -0
- package/dist/utils/package-manager.js.map +1 -0
- package/package.json +85 -73
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/config.test.ts +263 -0
- package/src/__tests__/connector-functions-generator.test.ts +288 -0
- package/src/__tests__/connector-mock-server.test.ts +439 -0
- package/src/__tests__/connector-model-bff.test.ts +162 -0
- package/src/__tests__/dev-seeds.test.ts +112 -0
- package/src/__tests__/dev.test.ts +148 -0
- package/src/__tests__/fixtures.ts +144 -0
- package/src/__tests__/functions-generator.test.ts +237 -0
- package/src/__tests__/init.test.ts +80 -0
- package/src/__tests__/machine.test.ts +212 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/model-parser.test.ts +72 -0
- package/src/__tests__/nextjs-generator.test.ts +97 -0
- package/src/__tests__/openapi-generator.test.ts +43 -0
- package/src/__tests__/package-manager.test.ts +189 -0
- package/src/__tests__/scaffold.test.ts +39 -0
- package/src/__tests__/string-utils.test.ts +75 -0
- package/src/__tests__/ui-generator.test.ts +144 -0
- package/src/__tests__/zod-mock-generator.test.ts +132 -0
- package/src/cli/commands/add-auth.ts +500 -0
- package/src/cli/commands/add-connector.ts +158 -0
- package/src/cli/commands/create-model.ts +62 -0
- package/src/cli/commands/dev-seeds.ts +358 -0
- package/src/cli/commands/dev.ts +962 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/init.ts +3371 -0
- package/src/cli/commands/provision.ts +193 -0
- package/src/cli/commands/scaffold.ts +1211 -0
- package/src/cli/index.ts +191 -0
- package/src/core/config.ts +308 -0
- package/src/core/mock/connector-mock-server.ts +555 -0
- package/src/core/mock/zod-mock-generator.ts +205 -0
- package/src/core/operations/create-model.ts +174 -0
- package/src/core/operations/runtime.ts +235 -0
- package/src/core/operations/scaffold-machine.ts +91 -0
- package/src/core/project/manifest.ts +402 -0
- package/src/core/project/validation.ts +221 -0
- package/src/core/scaffold/auth-generator.ts +1284 -0
- package/src/core/scaffold/connector-functions-generator.ts +1128 -0
- package/src/core/scaffold/functions-generator.ts +970 -0
- package/src/core/scaffold/model-parser.ts +841 -0
- package/src/core/scaffold/nextjs-generator.ts +370 -0
- package/src/core/scaffold/openapi-generator.ts +212 -0
- package/src/core/scaffold/ui-generator.ts +1061 -0
- package/src/database/base-model.ts +184 -0
- package/src/database/client.ts +140 -0
- package/src/database/repository.ts +104 -0
- package/src/database/runtime-check.ts +25 -0
- package/src/index.ts +27 -0
- package/src/machine/contracts.ts +17 -0
- package/src/machine/errors.ts +34 -0
- package/src/machine/index.ts +173 -0
- package/src/mcp/index.ts +185 -0
- package/src/types/index.ts +134 -0
- package/src/utils/package-manager.ts +229 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`generateDetailPage generates detail page (snapshot) 1`] = `
|
|
4
|
+
"'use client';
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { z } from 'zod/v4';
|
|
10
|
+
import { todoSchema } from '@myapp/shared';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
type Todo = z.infer<typeof todoSchema>;
|
|
14
|
+
|
|
15
|
+
export default function TodoDetailPage() {
|
|
16
|
+
const params = useParams();
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const [todo, setTodo] = useState<Todo | null>(null);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const id = params?.id as string;
|
|
26
|
+
if (!id) return;
|
|
27
|
+
|
|
28
|
+
fetch(\`/api/todo/\${id}\`)
|
|
29
|
+
.then((res) => {
|
|
30
|
+
if (!res.ok) throw new Error('Failed to fetch todo');
|
|
31
|
+
return res.json();
|
|
32
|
+
})
|
|
33
|
+
.then((data) => {
|
|
34
|
+
setTodo(data);
|
|
35
|
+
setLoading(false);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
setError(err.message);
|
|
39
|
+
setLoading(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
}, [params]);
|
|
44
|
+
|
|
45
|
+
const handleDelete = async () => {
|
|
46
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(\`/api/todo/\${params?.id}\`, {
|
|
50
|
+
method: 'DELETE',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) throw new Error('Failed to delete todo');
|
|
54
|
+
|
|
55
|
+
router.push('/todo');
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
alert(\`Error: \${err.message}\`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (loading) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
64
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (error || !todo) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
72
|
+
<div className="text-red-600 dark:text-red-400">Error: {error || 'Todo not found'}</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="container mx-auto px-4 py-8">
|
|
79
|
+
<div className="max-w-2xl mx-auto">
|
|
80
|
+
<div className="flex justify-between items-center mb-6">
|
|
81
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Todo Details</h1>
|
|
82
|
+
<div className="space-x-2">
|
|
83
|
+
<Link
|
|
84
|
+
href={\`/todo/\${todo.id}/edit\`}
|
|
85
|
+
className="inline-flex items-center bg-green-600 hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600 text-white px-4 py-2 rounded"
|
|
86
|
+
>
|
|
87
|
+
Edit
|
|
88
|
+
</Link>
|
|
89
|
+
<button
|
|
90
|
+
onClick={handleDelete}
|
|
91
|
+
className="inline-flex items-center bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white px-4 py-2 rounded"
|
|
92
|
+
>
|
|
93
|
+
Delete
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
99
|
+
<dl className="space-y-4">
|
|
100
|
+
<div>
|
|
101
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">id</dt>
|
|
102
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.id)}</dd>
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">title</dt>
|
|
106
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.title)}</dd>
|
|
107
|
+
</div>
|
|
108
|
+
<div>
|
|
109
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">description</dt>
|
|
110
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.description)}</dd>
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">completed</dt>
|
|
114
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.completed)}</dd>
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">createdAt</dt>
|
|
118
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.createdAt)}</dd>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">updatedAt</dt>
|
|
122
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{String(todo.updatedAt)}</dd>
|
|
123
|
+
</div>
|
|
124
|
+
</dl>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="mt-6">
|
|
128
|
+
<Link
|
|
129
|
+
href="/todo"
|
|
130
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
|
|
131
|
+
>
|
|
132
|
+
← Back to list
|
|
133
|
+
</Link>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
"
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
exports[`generateEditPage generates edit page (snapshot) 1`] = `
|
|
143
|
+
"'use client';
|
|
144
|
+
|
|
145
|
+
import { useEffect, useState } from 'react';
|
|
146
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
147
|
+
import TodoForm from '../../_components/TodoForm';
|
|
148
|
+
import { z } from 'zod/v4';
|
|
149
|
+
import { todoSchema } from '@myapp/shared';
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
type Todo = z.infer<typeof todoSchema>;
|
|
153
|
+
|
|
154
|
+
export default function EditTodoPage() {
|
|
155
|
+
const params = useParams();
|
|
156
|
+
|
|
157
|
+
const [todo, setTodo] = useState<Todo | null>(null);
|
|
158
|
+
const [loading, setLoading] = useState(true);
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
const id = params?.id as string;
|
|
164
|
+
if (!id) return;
|
|
165
|
+
|
|
166
|
+
fetch(\`/api/todo/\${id}\`)
|
|
167
|
+
.then((res) => res.json())
|
|
168
|
+
.then((data) => {
|
|
169
|
+
setTodo(data);
|
|
170
|
+
setLoading(false);
|
|
171
|
+
})
|
|
172
|
+
.catch(() => setLoading(false));
|
|
173
|
+
}, [params]);
|
|
174
|
+
|
|
175
|
+
if (loading) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
178
|
+
<div className="text-lg">Loading...</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!todo) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
186
|
+
<div className="text-red-600">Todo not found</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="container mx-auto px-4 py-8">
|
|
193
|
+
<div className="max-w-2xl mx-auto">
|
|
194
|
+
<h1 className="text-3xl font-bold mb-6">Edit Todo</h1>
|
|
195
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
196
|
+
<TodoForm initialData={todo} isEdit={true} />
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
"
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
exports[`generateFormComponent generates form component (snapshot) 1`] = `
|
|
206
|
+
"'use client';
|
|
207
|
+
|
|
208
|
+
import { useState, FormEvent } from 'react';
|
|
209
|
+
import { useRouter } from 'next/navigation';
|
|
210
|
+
import { z } from 'zod/v4';
|
|
211
|
+
import { todoSchema } from '@myapp/shared';
|
|
212
|
+
|
|
213
|
+
// Input schema: SwallowKit-managed fields (id, createdAt, updatedAt) are optional
|
|
214
|
+
// These fields are ignored by the backend and auto-managed
|
|
215
|
+
const TodoInputSchema = todoSchema.partial({ id: true, createdAt: true, updatedAt: true });
|
|
216
|
+
|
|
217
|
+
type Todo = z.infer<typeof todoSchema>;
|
|
218
|
+
|
|
219
|
+
interface TodoFormProps {
|
|
220
|
+
initialData?: Todo;
|
|
221
|
+
isEdit?: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default function TodoForm({ initialData, isEdit = false }: TodoFormProps) {
|
|
225
|
+
const router = useRouter();
|
|
226
|
+
const [loading, setLoading] = useState(false);
|
|
227
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
const [formData, setFormData] = useState({
|
|
232
|
+
title: initialData?.title ?? '',
|
|
233
|
+
description: initialData?.description ?? '',
|
|
234
|
+
completed: initialData?.completed ?? false,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const handleSubmit = async (e: FormEvent) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
setLoading(true);
|
|
240
|
+
setErrors({});
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Array フィールドをカンマ区切りから配列に変換
|
|
244
|
+
const submitData: any = { ...formData };
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
// Validate input (excluding SwallowKit-managed fields)
|
|
251
|
+
TodoInputSchema.parse(submitData);
|
|
252
|
+
|
|
253
|
+
const url = isEdit ? \`/api/todo/\${initialData!.id}\` : '/api/todo';
|
|
254
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
255
|
+
|
|
256
|
+
const res = await fetch(url, {
|
|
257
|
+
method,
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify(submitData),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
const errorData = await res.json();
|
|
264
|
+
throw new Error(errorData.error || 'Failed to save todo');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
router.push('/todo');
|
|
268
|
+
} catch (err: any) {
|
|
269
|
+
if (err.issues) {
|
|
270
|
+
// Zod validation errors
|
|
271
|
+
const fieldErrors: Record<string, string> = {};
|
|
272
|
+
err.issues.forEach((error: any) => {
|
|
273
|
+
const field = error.path[0];
|
|
274
|
+
fieldErrors[field] = error.message;
|
|
275
|
+
});
|
|
276
|
+
setErrors(fieldErrors);
|
|
277
|
+
} else {
|
|
278
|
+
alert(\`Error: \${err.message}\`);
|
|
279
|
+
}
|
|
280
|
+
setLoading(false);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
286
|
+
<div>
|
|
287
|
+
<label htmlFor="title" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
288
|
+
title *
|
|
289
|
+
</label>
|
|
290
|
+
<input
|
|
291
|
+
type="text"
|
|
292
|
+
id="title"
|
|
293
|
+
name="title"
|
|
294
|
+
value={formData.title}
|
|
295
|
+
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
296
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
297
|
+
required
|
|
298
|
+
/>
|
|
299
|
+
{errors.title && (
|
|
300
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.title}</p>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div>
|
|
305
|
+
<label htmlFor="description" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
306
|
+
description
|
|
307
|
+
</label>
|
|
308
|
+
<input
|
|
309
|
+
type="text"
|
|
310
|
+
id="description"
|
|
311
|
+
name="description"
|
|
312
|
+
value={formData.description}
|
|
313
|
+
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
314
|
+
className="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm px-3 py-2 focus:border-blue-500 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-400"
|
|
315
|
+
|
|
316
|
+
/>
|
|
317
|
+
{errors.description && (
|
|
318
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.description}</p>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div className="flex items-center">
|
|
323
|
+
<input
|
|
324
|
+
type="checkbox"
|
|
325
|
+
id="completed"
|
|
326
|
+
name="completed"
|
|
327
|
+
checked={formData.completed}
|
|
328
|
+
onChange={(e) => setFormData({ ...formData, completed: e.target.checked })}
|
|
329
|
+
className="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border border-gray-300 dark:border-gray-600 rounded"
|
|
330
|
+
/>
|
|
331
|
+
<label htmlFor="completed" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
|
|
332
|
+
completed
|
|
333
|
+
</label>
|
|
334
|
+
{errors.completed && (
|
|
335
|
+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.completed}</p>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className="flex gap-4">
|
|
340
|
+
<button
|
|
341
|
+
type="submit"
|
|
342
|
+
disabled={loading}
|
|
343
|
+
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50"
|
|
344
|
+
>
|
|
345
|
+
{loading ? 'Saving...' : isEdit ? 'Update' : 'Create'}
|
|
346
|
+
</button>
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
onClick={() => router.push('/todo')}
|
|
350
|
+
className="bg-gray-300 hover:bg-gray-400 text-gray-800 px-4 py-2 rounded"
|
|
351
|
+
>
|
|
352
|
+
Cancel
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
</form>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
"
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
exports[`generateListPage generates list page for basic model (snapshot) 1`] = `
|
|
362
|
+
"'use client';
|
|
363
|
+
|
|
364
|
+
import { useEffect, useState } from 'react';
|
|
365
|
+
import Link from 'next/link';
|
|
366
|
+
import { z } from 'zod/v4';
|
|
367
|
+
import { todoSchema } from '@myapp/shared';
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
type Todo = z.infer<typeof todoSchema>;
|
|
371
|
+
|
|
372
|
+
export default function TodoListPage() {
|
|
373
|
+
const [todos, setTodos] = useState<Todo[]>([]);
|
|
374
|
+
const [loading, setLoading] = useState(true);
|
|
375
|
+
const [error, setError] = useState<string | null>(null);
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
fetch('/api/todo')
|
|
381
|
+
.then((res) => {
|
|
382
|
+
if (!res.ok) throw new Error('Failed to fetch todos');
|
|
383
|
+
return res.json();
|
|
384
|
+
})
|
|
385
|
+
.then((data) => {
|
|
386
|
+
setTodos(data);
|
|
387
|
+
setLoading(false);
|
|
388
|
+
})
|
|
389
|
+
.catch((err) => {
|
|
390
|
+
setError(err.message);
|
|
391
|
+
setLoading(false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
}, []);
|
|
396
|
+
|
|
397
|
+
const handleDelete = async (id: string) => {
|
|
398
|
+
if (!confirm('Are you sure you want to delete this item?')) return;
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const res = await fetch(\`/api/todo/\${id}\`, {
|
|
402
|
+
method: 'DELETE',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!res.ok) throw new Error('Failed to delete todo');
|
|
406
|
+
|
|
407
|
+
setTodos(todos.filter((item) => item.id !== id));
|
|
408
|
+
} catch (err: any) {
|
|
409
|
+
alert(\`Error: \${err.message}\`);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (loading) {
|
|
414
|
+
return (
|
|
415
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
416
|
+
<div className="text-lg text-gray-900 dark:text-gray-100">Loading...</div>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (error) {
|
|
422
|
+
return (
|
|
423
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
424
|
+
<div className="text-red-600 dark:text-red-400">Error: {error}</div>
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div className="container mx-auto px-4 py-8">
|
|
431
|
+
<div className="mb-4">
|
|
432
|
+
<Link
|
|
433
|
+
href="/"
|
|
434
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 text-sm"
|
|
435
|
+
>
|
|
436
|
+
← Home
|
|
437
|
+
</Link>
|
|
438
|
+
</div>
|
|
439
|
+
<div className="flex justify-between items-center mb-6">
|
|
440
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Todo</h1>
|
|
441
|
+
<Link
|
|
442
|
+
href="/todo/new"
|
|
443
|
+
className="bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white px-4 py-2 rounded"
|
|
444
|
+
>
|
|
445
|
+
Create New
|
|
446
|
+
</Link>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
{todos.length === 0 ? (
|
|
450
|
+
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
451
|
+
No todos found. Create your first one!
|
|
452
|
+
</div>
|
|
453
|
+
) : (
|
|
454
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
|
455
|
+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
456
|
+
<thead className="bg-gray-50 dark:bg-gray-900">
|
|
457
|
+
<tr>
|
|
458
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
459
|
+
title
|
|
460
|
+
</th>
|
|
461
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
462
|
+
description
|
|
463
|
+
</th>
|
|
464
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
465
|
+
completed
|
|
466
|
+
</th>
|
|
467
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
468
|
+
Actions
|
|
469
|
+
</th>
|
|
470
|
+
</tr>
|
|
471
|
+
</thead>
|
|
472
|
+
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
473
|
+
{todos.map((item) => (
|
|
474
|
+
<tr key={item.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
475
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
476
|
+
{String(item.title)}
|
|
477
|
+
</td>
|
|
478
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
479
|
+
{String(item.description)}
|
|
480
|
+
</td>
|
|
481
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
482
|
+
{String(item.completed)}
|
|
483
|
+
</td>
|
|
484
|
+
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
485
|
+
<Link
|
|
486
|
+
href={\`/todo/\${item.id}\`}
|
|
487
|
+
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 mr-4"
|
|
488
|
+
>
|
|
489
|
+
View
|
|
490
|
+
</Link>
|
|
491
|
+
<Link
|
|
492
|
+
href={\`/todo/\${item.id}/edit\`}
|
|
493
|
+
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 mr-4"
|
|
494
|
+
>
|
|
495
|
+
Edit
|
|
496
|
+
</Link>
|
|
497
|
+
<button
|
|
498
|
+
onClick={() => handleDelete(item.id)}
|
|
499
|
+
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
|
500
|
+
>
|
|
501
|
+
Delete
|
|
502
|
+
</button>
|
|
503
|
+
</td>
|
|
504
|
+
</tr>
|
|
505
|
+
))}
|
|
506
|
+
</tbody>
|
|
507
|
+
</table>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
"
|
|
514
|
+
`;
|
|
515
|
+
|
|
516
|
+
exports[`generateNewPage generates new page (snapshot) 1`] = `
|
|
517
|
+
"import TodoForm from '../_components/TodoForm';
|
|
518
|
+
|
|
519
|
+
export default function NewTodoPage() {
|
|
520
|
+
return (
|
|
521
|
+
<div className="container mx-auto px-4 py-8">
|
|
522
|
+
<div className="max-w-2xl mx-auto">
|
|
523
|
+
<h1 className="text-3xl font-bold mb-6">Create New Todo</h1>
|
|
524
|
+
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-6">
|
|
525
|
+
<TodoForm />
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
"
|
|
532
|
+
`;
|