@compilr-dev/factory 0.1.6 → 0.1.8
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/dist/factory/registry.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/toolkits/next-prisma/api-routes.d.ts +10 -0
- package/dist/toolkits/next-prisma/api-routes.js +155 -0
- package/dist/toolkits/next-prisma/config.d.ts +9 -0
- package/dist/toolkits/next-prisma/config.js +139 -0
- package/dist/toolkits/next-prisma/dashboard.d.ts +9 -0
- package/dist/toolkits/next-prisma/dashboard.js +87 -0
- package/dist/toolkits/next-prisma/entity-components.d.ts +10 -0
- package/dist/toolkits/next-prisma/entity-components.js +217 -0
- package/dist/toolkits/next-prisma/entity-pages.d.ts +12 -0
- package/dist/toolkits/next-prisma/entity-pages.js +348 -0
- package/dist/toolkits/next-prisma/helpers.d.ts +13 -0
- package/dist/toolkits/next-prisma/helpers.js +37 -0
- package/dist/toolkits/next-prisma/index.d.ts +9 -0
- package/dist/toolkits/next-prisma/index.js +57 -0
- package/dist/toolkits/next-prisma/layout.d.ts +9 -0
- package/dist/toolkits/next-prisma/layout.js +157 -0
- package/dist/toolkits/next-prisma/prisma.d.ts +8 -0
- package/dist/toolkits/next-prisma/prisma.js +76 -0
- package/dist/toolkits/next-prisma/seed.d.ts +9 -0
- package/dist/toolkits/next-prisma/seed.js +100 -0
- package/dist/toolkits/next-prisma/static.d.ts +8 -0
- package/dist/toolkits/next-prisma/static.js +61 -0
- package/dist/toolkits/next-prisma/types-gen.d.ts +10 -0
- package/dist/toolkits/next-prisma/types-gen.js +62 -0
- package/dist/toolkits/react-node/api.js +96 -13
- package/dist/toolkits/react-node/config.d.ts +1 -4
- package/dist/toolkits/react-node/config.js +3 -84
- package/dist/toolkits/react-node/entity.js +64 -22
- package/dist/toolkits/react-node/helpers.d.ts +2 -23
- package/dist/toolkits/react-node/helpers.js +2 -67
- package/dist/toolkits/react-node/seed.d.ts +4 -3
- package/dist/toolkits/react-node/seed.js +4 -111
- package/dist/toolkits/react-node/shared.d.ts +2 -3
- package/dist/toolkits/react-node/shared.js +2 -115
- package/dist/toolkits/react-node/types-gen.js +26 -0
- package/dist/toolkits/shared/color-utils.d.ts +10 -0
- package/dist/toolkits/shared/color-utils.js +85 -0
- package/dist/toolkits/shared/components.d.ts +12 -0
- package/dist/toolkits/shared/components.js +121 -0
- package/dist/toolkits/shared/helpers.d.ts +28 -0
- package/dist/toolkits/shared/helpers.js +72 -0
- package/dist/toolkits/shared/index.d.ts +9 -0
- package/dist/toolkits/shared/index.js +9 -0
- package/dist/toolkits/shared/seed-data.d.ts +18 -0
- package/dist/toolkits/shared/seed-data.js +119 -0
- package/package.json +1 -1
|
@@ -26,7 +26,12 @@ function generateListPage(model, entity) {
|
|
|
26
26
|
const hasFilters = enumFields.length > 0;
|
|
27
27
|
const tableHeaders = entity.fields
|
|
28
28
|
.slice(0, 5)
|
|
29
|
-
.map((f) => ` <th
|
|
29
|
+
.map((f) => ` <th
|
|
30
|
+
className="cursor-pointer select-none px-4 py-2 text-left text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
31
|
+
onClick={() => handleSort('${f.name}')}
|
|
32
|
+
>
|
|
33
|
+
${f.label}{sortField === '${f.name}' ? (sortDirection === 'asc' ? ' ↑' : ' ↓') : ''}
|
|
34
|
+
</th>`)
|
|
30
35
|
.join('\n');
|
|
31
36
|
const tableCells = entity.fields
|
|
32
37
|
.slice(0, 5)
|
|
@@ -45,16 +50,27 @@ ${hasFilters ? `const filterOptions = [\n${filterDefs}\n];\n` : ''}
|
|
|
45
50
|
export default function ${entity.name}List() {
|
|
46
51
|
const [items, setItems] = useState<${entity.name}[]>([]);
|
|
47
52
|
const [view, setView] = useState<'card' | 'list'>('card');
|
|
48
|
-
const [search, setSearch] = useState('')
|
|
53
|
+
const [search, setSearch] = useState('');
|
|
54
|
+
const [sortField, setSortField] = useState<string>('');
|
|
55
|
+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');${hasFilters ? '\n const [filters, setFilters] = useState<Record<string, string>>({});' : ''}
|
|
49
56
|
|
|
50
57
|
useEffect(() => {
|
|
51
58
|
fetch('${api}')
|
|
52
59
|
.then((r) => r.json())
|
|
53
|
-
.then((data: ${entity.name}[]) => setItems(data));
|
|
60
|
+
.then((res: { data: ${entity.name}[]; total: number }) => setItems(res.data));
|
|
54
61
|
}, []);
|
|
55
62
|
|
|
56
63
|
const onSearch = useCallback((q: string) => setSearch(q), []);
|
|
57
64
|
|
|
65
|
+
const handleSort = useCallback((field: string) => {
|
|
66
|
+
if (sortField === field) {
|
|
67
|
+
setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
68
|
+
} else {
|
|
69
|
+
setSortField(field);
|
|
70
|
+
setSortDirection('asc');
|
|
71
|
+
}
|
|
72
|
+
}, [sortField]);
|
|
73
|
+
|
|
58
74
|
const filtered = items.filter((item) => {
|
|
59
75
|
if (search) {
|
|
60
76
|
const q = search.toLowerCase();
|
|
@@ -71,6 +87,14 @@ export default function ${entity.name}List() {
|
|
|
71
87
|
return true;
|
|
72
88
|
});
|
|
73
89
|
|
|
90
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
91
|
+
if (!sortField) return 0;
|
|
92
|
+
const aVal = (a as Record<string, unknown>)[sortField];
|
|
93
|
+
const bVal = (b as Record<string, unknown>)[sortField];
|
|
94
|
+
const cmp = String(aVal ?? '').localeCompare(String(bVal ?? ''), undefined, { numeric: true });
|
|
95
|
+
return sortDirection === 'asc' ? cmp : -cmp;
|
|
96
|
+
});
|
|
97
|
+
|
|
74
98
|
return (
|
|
75
99
|
<div>
|
|
76
100
|
<div className="mb-6 flex items-center justify-between">
|
|
@@ -93,7 +117,7 @@ export default function ${entity.name}List() {
|
|
|
93
117
|
|
|
94
118
|
{view === 'card' ? (
|
|
95
119
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
96
|
-
{
|
|
120
|
+
{sorted.map((item) => (
|
|
97
121
|
<${entity.name}Card key={item.id} item={item} />
|
|
98
122
|
))}
|
|
99
123
|
</div>
|
|
@@ -105,7 +129,7 @@ ${tableHeaders}
|
|
|
105
129
|
</tr>
|
|
106
130
|
</thead>
|
|
107
131
|
<tbody>
|
|
108
|
-
{
|
|
132
|
+
{sorted.map((item) => (
|
|
109
133
|
<tr key={item.id} className="border-b border-gray-100 hover:bg-gray-50 dark:border-gray-800 dark:hover:bg-gray-800/50">
|
|
110
134
|
${tableCells}
|
|
111
135
|
<td className="px-4 py-2">
|
|
@@ -160,22 +184,44 @@ function generateDetailPage(model, entity) {
|
|
|
160
184
|
const [${stateVar}, set${targetEntity.pluralName}] = useState<${rel.target}[]>([]);`;
|
|
161
185
|
})
|
|
162
186
|
.join('');
|
|
163
|
-
|
|
187
|
+
// Build ?include= param for hasMany targets
|
|
188
|
+
const includeTargets = hmRels
|
|
189
|
+
.map((rel) => {
|
|
190
|
+
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
191
|
+
return targetEntity ? toCamelCase(targetEntity.pluralName) : '';
|
|
192
|
+
})
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
const includeParam = includeTargets.length > 0 ? `?include=${includeTargets.join(',')}` : '';
|
|
195
|
+
const hasManySetters = hmRels
|
|
164
196
|
.map((rel) => {
|
|
165
197
|
const targetEntity = model.entities.find((e) => e.name === rel.target);
|
|
166
198
|
if (!targetEntity)
|
|
167
199
|
return '';
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
type: 'belongsTo',
|
|
171
|
-
target: entity.name,
|
|
172
|
-
});
|
|
173
|
-
return `
|
|
174
|
-
fetch('${fetchApi}')
|
|
175
|
-
.then((r) => r.json())
|
|
176
|
-
.then((data: ${rel.target}[]) => set${targetEntity.pluralName}(data.filter((d) => d.${fk} === Number(id))));`;
|
|
200
|
+
const varName = toCamelCase(targetEntity.pluralName);
|
|
201
|
+
return ` set${targetEntity.pluralName}(data.${varName} ?? []);`;
|
|
177
202
|
})
|
|
178
|
-
.join('');
|
|
203
|
+
.join('\n');
|
|
204
|
+
// Build the useEffect block
|
|
205
|
+
let useEffectBlock;
|
|
206
|
+
if (hmRels.length > 0) {
|
|
207
|
+
useEffectBlock =
|
|
208
|
+
' useEffect(() => {\n' +
|
|
209
|
+
` fetch(\`${api}/\${id as string}${includeParam}\`)\n` +
|
|
210
|
+
' .then((r) => r.json())\n' +
|
|
211
|
+
' .then((data: Record<string, unknown>) => {\n' +
|
|
212
|
+
` setItem(data as unknown as ${entity.name});\n` +
|
|
213
|
+
hasManySetters +
|
|
214
|
+
'\n });\n' +
|
|
215
|
+
' }, [id]);';
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
useEffectBlock =
|
|
219
|
+
' useEffect(() => {\n' +
|
|
220
|
+
` fetch(\`${api}/\${id as string}\`)\n` +
|
|
221
|
+
' .then((r) => r.json())\n' +
|
|
222
|
+
` .then((data: ${entity.name}) => setItem(data));\n` +
|
|
223
|
+
' }, [id]);';
|
|
224
|
+
}
|
|
179
225
|
const hasManyImports = hmRels.map((rel) => rel.target).join(', ');
|
|
180
226
|
const hasManyTypeImport = hmRels.length > 0 ? `, ${hasManyImports}` : '';
|
|
181
227
|
const hasManyRender = hmRels
|
|
@@ -217,11 +263,7 @@ export default function ${entity.name}Detail() {
|
|
|
217
263
|
const navigate = useNavigate();
|
|
218
264
|
const [item, setItem] = useState<${entity.name} | null>(null);${hasManySection}
|
|
219
265
|
|
|
220
|
-
|
|
221
|
-
fetch(\`${api}/\${id as string}\`)
|
|
222
|
-
.then((r) => r.json())
|
|
223
|
-
.then((data: ${entity.name}) => setItem(data));${hasManyFetches}
|
|
224
|
-
}, [id]);
|
|
266
|
+
${useEffectBlock}
|
|
225
267
|
|
|
226
268
|
if (!item) return <div className="p-6">Loading...</div>;
|
|
227
269
|
|
|
@@ -330,7 +372,7 @@ function generateForm(model, entity) {
|
|
|
330
372
|
if (!targetEntity)
|
|
331
373
|
return '';
|
|
332
374
|
const targetApi = apiPath(targetEntity);
|
|
333
|
-
return ` fetch('${targetApi}').then((r) => r.json()).then((
|
|
375
|
+
return ` fetch('${targetApi}').then((r) => r.json()).then((res: { data: ${rel.target}[]; total: number }) => set${rel.target}Options(res.data));`;
|
|
334
376
|
})
|
|
335
377
|
.join('\n');
|
|
336
378
|
const relTypeImports = btoRels.map((r) => r.target);
|
|
@@ -1,27 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React+Node Toolkit — Shared Helpers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports from the shared helpers module.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
/** Indent every line of a multi-line string. */
|
|
8
|
-
export declare function indent(text: string, spaces: number): string;
|
|
9
|
-
/** Map a model FieldType to a TypeScript type string. */
|
|
10
|
-
export declare function tsType(field: Field): string;
|
|
11
|
-
/** Get the FK field name for a belongsTo relationship. */
|
|
12
|
-
export declare function fkFieldName(rel: Relationship): string;
|
|
13
|
-
/** Get belongsTo relationships for an entity. */
|
|
14
|
-
export declare function belongsToRels(entity: Entity): readonly Relationship[];
|
|
15
|
-
/** Get hasMany relationships for an entity. */
|
|
16
|
-
export declare function hasManyRels(entity: Entity): readonly Relationship[];
|
|
17
|
-
/** Generate a route path from entity plural name. */
|
|
18
|
-
export declare function routePath(entity: Entity): string;
|
|
19
|
-
/** Generate an API path from entity plural name. */
|
|
20
|
-
export declare function apiPath(entity: Entity): string;
|
|
21
|
-
/** Map a model FieldType to an HTML input type. */
|
|
22
|
-
export declare function inputType(field: Field): string;
|
|
23
|
-
/** Generate import statement lines. */
|
|
24
|
-
export declare function generateImports(imports: Array<{
|
|
25
|
-
from: string;
|
|
26
|
-
names: string[];
|
|
27
|
-
}>): string;
|
|
6
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|
|
@@ -1,71 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React+Node Toolkit — Shared Helpers
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports from the shared helpers module.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
/** Indent every line of a multi-line string. */
|
|
8
|
-
export function indent(text, spaces) {
|
|
9
|
-
const pad = ' '.repeat(spaces);
|
|
10
|
-
return text
|
|
11
|
-
.split('\n')
|
|
12
|
-
.map((line) => (line.trim() === '' ? '' : pad + line))
|
|
13
|
-
.join('\n');
|
|
14
|
-
}
|
|
15
|
-
/** Map a model FieldType to a TypeScript type string. */
|
|
16
|
-
export function tsType(field) {
|
|
17
|
-
switch (field.type) {
|
|
18
|
-
case 'string':
|
|
19
|
-
case 'enum':
|
|
20
|
-
return 'string';
|
|
21
|
-
case 'number':
|
|
22
|
-
return 'number';
|
|
23
|
-
case 'boolean':
|
|
24
|
-
return 'boolean';
|
|
25
|
-
case 'date':
|
|
26
|
-
return 'string'; // ISO date string
|
|
27
|
-
default:
|
|
28
|
-
return 'string';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/** Get the FK field name for a belongsTo relationship. */
|
|
32
|
-
export function fkFieldName(rel) {
|
|
33
|
-
return rel.fieldName ?? toCamelCase(rel.target) + 'Id';
|
|
34
|
-
}
|
|
35
|
-
/** Get belongsTo relationships for an entity. */
|
|
36
|
-
export function belongsToRels(entity) {
|
|
37
|
-
return entity.relationships.filter((r) => r.type === 'belongsTo');
|
|
38
|
-
}
|
|
39
|
-
/** Get hasMany relationships for an entity. */
|
|
40
|
-
export function hasManyRels(entity) {
|
|
41
|
-
return entity.relationships.filter((r) => r.type === 'hasMany');
|
|
42
|
-
}
|
|
43
|
-
/** Generate a route path from entity plural name. */
|
|
44
|
-
export function routePath(entity) {
|
|
45
|
-
return '/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
46
|
-
}
|
|
47
|
-
/** Generate an API path from entity plural name. */
|
|
48
|
-
export function apiPath(entity) {
|
|
49
|
-
return '/api/' + toKebabCase(entity.pluralName).toLowerCase();
|
|
50
|
-
}
|
|
51
|
-
/** Map a model FieldType to an HTML input type. */
|
|
52
|
-
export function inputType(field) {
|
|
53
|
-
switch (field.type) {
|
|
54
|
-
case 'string':
|
|
55
|
-
return 'text';
|
|
56
|
-
case 'number':
|
|
57
|
-
return 'number';
|
|
58
|
-
case 'boolean':
|
|
59
|
-
return 'checkbox';
|
|
60
|
-
case 'date':
|
|
61
|
-
return 'date';
|
|
62
|
-
case 'enum':
|
|
63
|
-
return 'select';
|
|
64
|
-
default:
|
|
65
|
-
return 'text';
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** Generate import statement lines. */
|
|
69
|
-
export function generateImports(imports) {
|
|
70
|
-
return imports.map((i) => `import { ${i.names.join(', ')} } from '${i.from}';`).join('\n');
|
|
71
|
-
}
|
|
6
|
+
export { indent, tsType, fkFieldName, belongsToRels, hasManyRels, routePath, apiPath, inputType, generateImports, } from '../shared/helpers.js';
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* React+Node Toolkit — Seed Data Generator
|
|
3
3
|
*
|
|
4
4
|
* Generates realistic mock data based on field semantics.
|
|
5
|
-
*
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in JavaScript array format for in-memory data stores.
|
|
6
7
|
*/
|
|
7
8
|
import type { ApplicationModel, Entity } from '../../model/types.js';
|
|
8
|
-
|
|
9
|
-
/** Generate seed data items for a single entity. */
|
|
9
|
+
import { SEED_COUNT } from '../shared/seed-data.js';
|
|
10
|
+
/** Generate seed data items for a single entity (JS array format). */
|
|
10
11
|
export declare function generateSeedData(model: ApplicationModel, entity: Entity): string;
|
|
11
12
|
export { SEED_COUNT };
|
|
@@ -2,120 +2,13 @@
|
|
|
2
2
|
* React+Node Toolkit — Seed Data Generator
|
|
3
3
|
*
|
|
4
4
|
* Generates realistic mock data based on field semantics.
|
|
5
|
-
*
|
|
5
|
+
* Uses shared generateFieldValue for deterministic values,
|
|
6
|
+
* wraps them in JavaScript array format for in-memory data stores.
|
|
6
7
|
*/
|
|
7
8
|
import { toCamelCase } from '../../model/naming.js';
|
|
8
9
|
import { belongsToRels, fkFieldName } from './helpers.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'Bob Smith',
|
|
12
|
-
'Carol Davis',
|
|
13
|
-
'David Lee',
|
|
14
|
-
'Eva Martinez',
|
|
15
|
-
'Frank Wilson',
|
|
16
|
-
'Grace Chen',
|
|
17
|
-
'Henry Brown',
|
|
18
|
-
'Iris Kim',
|
|
19
|
-
'Jack Taylor',
|
|
20
|
-
];
|
|
21
|
-
const COMPANY_NAMES = [
|
|
22
|
-
'Acme Corp',
|
|
23
|
-
'TechVista Inc',
|
|
24
|
-
'BlueSky Solutions',
|
|
25
|
-
'Greenfield LLC',
|
|
26
|
-
'Summit Dynamics',
|
|
27
|
-
'Cascade Systems',
|
|
28
|
-
'Meridian Labs',
|
|
29
|
-
'Atlas Ventures',
|
|
30
|
-
];
|
|
31
|
-
const PRODUCT_NAMES = [
|
|
32
|
-
'Premium Widget',
|
|
33
|
-
'Standard Package',
|
|
34
|
-
'Enterprise Suite',
|
|
35
|
-
'Starter Kit',
|
|
36
|
-
'Pro Bundle',
|
|
37
|
-
'Deluxe Edition',
|
|
38
|
-
'Basic Plan',
|
|
39
|
-
'Advanced Toolkit',
|
|
40
|
-
];
|
|
41
|
-
const DESCRIPTIONS = [
|
|
42
|
-
'A comprehensive solution for modern needs.',
|
|
43
|
-
'Designed for efficiency and reliability.',
|
|
44
|
-
'Built with quality and care.',
|
|
45
|
-
'Perfect for teams of any size.',
|
|
46
|
-
'Streamlined for maximum productivity.',
|
|
47
|
-
'Industry-leading performance.',
|
|
48
|
-
'Trusted by thousands of users.',
|
|
49
|
-
'Next-generation technology.',
|
|
50
|
-
];
|
|
51
|
-
const EMAILS = [
|
|
52
|
-
'alice@example.com',
|
|
53
|
-
'bob@example.com',
|
|
54
|
-
'carol@example.com',
|
|
55
|
-
'david@example.com',
|
|
56
|
-
'eva@example.com',
|
|
57
|
-
'frank@example.com',
|
|
58
|
-
'grace@example.com',
|
|
59
|
-
'henry@example.com',
|
|
60
|
-
];
|
|
61
|
-
const SEED_COUNT = 8;
|
|
62
|
-
function generateFieldValue(field, index, entityName) {
|
|
63
|
-
const lowerName = field.name.toLowerCase();
|
|
64
|
-
const lowerEntity = entityName.toLowerCase();
|
|
65
|
-
switch (field.type) {
|
|
66
|
-
case 'string':
|
|
67
|
-
if (lowerName.includes('email'))
|
|
68
|
-
return `'${EMAILS[index % EMAILS.length]}'`;
|
|
69
|
-
if (lowerName.includes('name') || lowerName === 'title') {
|
|
70
|
-
if (lowerEntity.includes('customer') ||
|
|
71
|
-
lowerEntity.includes('user') ||
|
|
72
|
-
lowerEntity.includes('person') ||
|
|
73
|
-
lowerEntity.includes('employee')) {
|
|
74
|
-
return `'${PERSON_NAMES[index % PERSON_NAMES.length]}'`;
|
|
75
|
-
}
|
|
76
|
-
if (lowerEntity.includes('company') || lowerEntity.includes('organization')) {
|
|
77
|
-
return `'${COMPANY_NAMES[index % COMPANY_NAMES.length]}'`;
|
|
78
|
-
}
|
|
79
|
-
return `'${PRODUCT_NAMES[index % PRODUCT_NAMES.length]}'`;
|
|
80
|
-
}
|
|
81
|
-
if (lowerName.includes('description') ||
|
|
82
|
-
lowerName.includes('note') ||
|
|
83
|
-
lowerName.includes('comment')) {
|
|
84
|
-
return `'${DESCRIPTIONS[index % DESCRIPTIONS.length]}'`;
|
|
85
|
-
}
|
|
86
|
-
if (lowerName.includes('phone'))
|
|
87
|
-
return `'555-010${String(index)}'`;
|
|
88
|
-
if (lowerName.includes('address'))
|
|
89
|
-
return `'${String(100 + index * 10)} Main St'`;
|
|
90
|
-
return `'${field.label} ${String(index + 1)}'`;
|
|
91
|
-
case 'number':
|
|
92
|
-
if (lowerName.includes('price') || lowerName.includes('cost') || lowerName.includes('amount'))
|
|
93
|
-
return String(10 + index * 15 + 0.99);
|
|
94
|
-
if (lowerName.includes('quantity') || lowerName.includes('count'))
|
|
95
|
-
return String(1 + (index % 10));
|
|
96
|
-
if (lowerName.includes('age'))
|
|
97
|
-
return String(22 + index * 5);
|
|
98
|
-
if (lowerName.includes('size') || lowerName.includes('capacity'))
|
|
99
|
-
return String(2 + (index % 8));
|
|
100
|
-
return String(index + 1);
|
|
101
|
-
case 'boolean':
|
|
102
|
-
return index % 2 === 0 ? 'true' : 'false';
|
|
103
|
-
case 'date': {
|
|
104
|
-
const base = new Date('2025-06-15');
|
|
105
|
-
base.setDate(base.getDate() + (index * 3 - 10));
|
|
106
|
-
return `'${base.toISOString().split('T')[0]}'`;
|
|
107
|
-
}
|
|
108
|
-
case 'enum': {
|
|
109
|
-
const values = field.enumValues ?? [];
|
|
110
|
-
if (values.length === 0)
|
|
111
|
-
return "''";
|
|
112
|
-
return `'${values[index % values.length]}'`;
|
|
113
|
-
}
|
|
114
|
-
default:
|
|
115
|
-
return `'${field.label} ${String(index + 1)}'`;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/** Generate seed data items for a single entity. */
|
|
10
|
+
import { generateFieldValue, SEED_COUNT } from '../shared/seed-data.js';
|
|
11
|
+
/** Generate seed data items for a single entity (JS array format). */
|
|
119
12
|
export function generateSeedData(model, entity) {
|
|
120
13
|
const varName = toCamelCase(entity.pluralName);
|
|
121
14
|
const rels = belongsToRels(entity);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React+Node Toolkit — Shared Components Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports from the shared components module.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
export declare function generateSharedComponents(): FactoryFile[];
|
|
6
|
+
export { generateSharedComponents } from '../shared/components.js';
|
|
@@ -1,119 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React+Node Toolkit — Shared Components Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Re-exports from the shared components module.
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
7
|
-
return [generateViewToggle(), generateSearchBar(), generateFilterBar()];
|
|
8
|
-
}
|
|
9
|
-
function generateViewToggle() {
|
|
10
|
-
return {
|
|
11
|
-
path: 'src/components/shared/ViewToggle.tsx',
|
|
12
|
-
content: `interface ViewToggleProps {
|
|
13
|
-
view: 'card' | 'list';
|
|
14
|
-
onChange: (view: 'card' | 'list') => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export default function ViewToggle({ view, onChange }: ViewToggleProps) {
|
|
18
|
-
return (
|
|
19
|
-
<div className="flex rounded-lg border border-gray-300 dark:border-gray-600">
|
|
20
|
-
<button
|
|
21
|
-
onClick={() => onChange('card')}
|
|
22
|
-
className={\`px-3 py-1 text-sm \${view === 'card' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
23
|
-
>
|
|
24
|
-
Grid
|
|
25
|
-
</button>
|
|
26
|
-
<button
|
|
27
|
-
onClick={() => onChange('list')}
|
|
28
|
-
className={\`px-3 py-1 text-sm \${view === 'list' ? 'bg-primary text-white' : 'text-gray-600 dark:text-gray-300'}\`}
|
|
29
|
-
>
|
|
30
|
-
List
|
|
31
|
-
</button>
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function generateSearchBar() {
|
|
39
|
-
return {
|
|
40
|
-
path: 'src/components/shared/SearchBar.tsx',
|
|
41
|
-
content: `import { useState, useEffect } from 'react';
|
|
42
|
-
|
|
43
|
-
interface SearchBarProps {
|
|
44
|
-
onSearch: (query: string) => void;
|
|
45
|
-
placeholder?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export default function SearchBar({ onSearch, placeholder = 'Search...' }: SearchBarProps) {
|
|
49
|
-
const [value, setValue] = useState('');
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const timer = setTimeout(() => onSearch(value), 300);
|
|
53
|
-
return () => clearTimeout(timer);
|
|
54
|
-
}, [value, onSearch]);
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className="relative">
|
|
58
|
-
<input
|
|
59
|
-
type="text"
|
|
60
|
-
value={value}
|
|
61
|
-
onChange={(e) => setValue(e.target.value)}
|
|
62
|
-
placeholder={placeholder}
|
|
63
|
-
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:border-primary focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
64
|
-
/>
|
|
65
|
-
{value && (
|
|
66
|
-
<button
|
|
67
|
-
onClick={() => setValue('')}
|
|
68
|
-
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
69
|
-
>
|
|
70
|
-
×
|
|
71
|
-
</button>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
`,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function generateFilterBar() {
|
|
80
|
-
return {
|
|
81
|
-
path: 'src/components/shared/FilterBar.tsx',
|
|
82
|
-
content: `interface FilterOption {
|
|
83
|
-
label: string;
|
|
84
|
-
field: string;
|
|
85
|
-
values: string[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
interface FilterBarProps {
|
|
89
|
-
filters: FilterOption[];
|
|
90
|
-
active: Record<string, string>;
|
|
91
|
-
onChange: (field: string, value: string) => void;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export default function FilterBar({ filters, active, onChange }: FilterBarProps) {
|
|
95
|
-
if (filters.length === 0) return null;
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className="flex flex-wrap gap-2">
|
|
99
|
-
{filters.map((filter) => (
|
|
100
|
-
<select
|
|
101
|
-
key={filter.field}
|
|
102
|
-
value={active[filter.field] ?? ''}
|
|
103
|
-
onChange={(e) => onChange(filter.field, e.target.value)}
|
|
104
|
-
className="rounded-lg border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-white"
|
|
105
|
-
>
|
|
106
|
-
<option value="">{filter.label}: All</option>
|
|
107
|
-
{filter.values.map((v) => (
|
|
108
|
-
<option key={v} value={v}>
|
|
109
|
-
{v}
|
|
110
|
-
</option>
|
|
111
|
-
))}
|
|
112
|
-
</select>
|
|
113
|
-
))}
|
|
114
|
-
</div>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
`,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
6
|
+
export { generateSharedComponents } from '../shared/components.js';
|
|
@@ -4,8 +4,34 @@
|
|
|
4
4
|
* Generates src/types/index.ts with interfaces for each entity.
|
|
5
5
|
*/
|
|
6
6
|
import { tsType, fkFieldName, belongsToRels } from './helpers.js';
|
|
7
|
+
const SHARED_TYPES = `// --- Shared API Types ---
|
|
8
|
+
|
|
9
|
+
export interface PaginatedResponse<T> {
|
|
10
|
+
data: T[];
|
|
11
|
+
total: number;
|
|
12
|
+
page: number;
|
|
13
|
+
pageSize: number;
|
|
14
|
+
totalPages: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ApiResponse<T> {
|
|
18
|
+
data: T;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ApiListResponse<T> {
|
|
23
|
+
data: T[];
|
|
24
|
+
total: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SortParams {
|
|
28
|
+
field: string;
|
|
29
|
+
direction: 'asc' | 'desc';
|
|
30
|
+
}`;
|
|
7
31
|
export function generateTypesFile(model) {
|
|
8
32
|
const lines = ['// Auto-generated TypeScript types from Application Model', ''];
|
|
33
|
+
lines.push(SHARED_TYPES);
|
|
34
|
+
lines.push('');
|
|
9
35
|
for (const entity of model.entities) {
|
|
10
36
|
lines.push(generateInterface(entity));
|
|
11
37
|
lines.push('');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Color Utilities
|
|
3
|
+
*
|
|
4
|
+
* Hex-to-HSL conversion and Tailwind color shade generation.
|
|
5
|
+
* Used by any toolkit that needs to generate a color palette from a primary color.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SHADE_LIGHTNESS: Record<string, number>;
|
|
8
|
+
export declare function hexToHsl(hex: string): [number, number, number];
|
|
9
|
+
export declare function hslToHex(h: number, s: number, l: number): string;
|
|
10
|
+
export declare function generateColorShades(hex: string): string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Color Utilities
|
|
3
|
+
*
|
|
4
|
+
* Hex-to-HSL conversion and Tailwind color shade generation.
|
|
5
|
+
* Used by any toolkit that needs to generate a color palette from a primary color.
|
|
6
|
+
*/
|
|
7
|
+
export const SHADE_LIGHTNESS = {
|
|
8
|
+
'50': 97,
|
|
9
|
+
'100': 94,
|
|
10
|
+
'200': 86,
|
|
11
|
+
'300': 77,
|
|
12
|
+
'400': 66,
|
|
13
|
+
'500': 55,
|
|
14
|
+
'600': 44,
|
|
15
|
+
'700': 36,
|
|
16
|
+
'800': 27,
|
|
17
|
+
'900': 20,
|
|
18
|
+
'950': 14,
|
|
19
|
+
};
|
|
20
|
+
export function hexToHsl(hex) {
|
|
21
|
+
const raw = hex.replace('#', '');
|
|
22
|
+
const r = parseInt(raw.substring(0, 2), 16) / 255;
|
|
23
|
+
const g = parseInt(raw.substring(2, 4), 16) / 255;
|
|
24
|
+
const b = parseInt(raw.substring(4, 6), 16) / 255;
|
|
25
|
+
const max = Math.max(r, g, b);
|
|
26
|
+
const min = Math.min(r, g, b);
|
|
27
|
+
const l = (max + min) / 2;
|
|
28
|
+
if (max === min)
|
|
29
|
+
return [0, 0, Math.round(l * 100)];
|
|
30
|
+
const d = max - min;
|
|
31
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
32
|
+
let h = 0;
|
|
33
|
+
if (max === r)
|
|
34
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
35
|
+
else if (max === g)
|
|
36
|
+
h = ((b - r) / d + 2) / 6;
|
|
37
|
+
else
|
|
38
|
+
h = ((r - g) / d + 4) / 6;
|
|
39
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
40
|
+
}
|
|
41
|
+
export function hslToHex(h, s, l) {
|
|
42
|
+
const sN = s / 100;
|
|
43
|
+
const lN = l / 100;
|
|
44
|
+
const c = (1 - Math.abs(2 * lN - 1)) * sN;
|
|
45
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
46
|
+
const m = lN - c / 2;
|
|
47
|
+
let r = 0, g = 0, b = 0;
|
|
48
|
+
if (h < 60)
|
|
49
|
+
[r, g, b] = [c, x, 0];
|
|
50
|
+
else if (h < 120)
|
|
51
|
+
[r, g, b] = [x, c, 0];
|
|
52
|
+
else if (h < 180)
|
|
53
|
+
[r, g, b] = [0, c, x];
|
|
54
|
+
else if (h < 240)
|
|
55
|
+
[r, g, b] = [0, x, c];
|
|
56
|
+
else if (h < 300)
|
|
57
|
+
[r, g, b] = [x, 0, c];
|
|
58
|
+
else
|
|
59
|
+
[r, g, b] = [c, 0, x];
|
|
60
|
+
const toHex = (v) => Math.round((v + m) * 255)
|
|
61
|
+
.toString(16)
|
|
62
|
+
.padStart(2, '0');
|
|
63
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
64
|
+
}
|
|
65
|
+
export function generateColorShades(hex) {
|
|
66
|
+
const [h, s] = hexToHsl(hex);
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const [shade, lightness] of Object.entries(SHADE_LIGHTNESS)) {
|
|
69
|
+
entries.push(` ${shade}: '${hslToHex(h, s, lightness)}',`);
|
|
70
|
+
}
|
|
71
|
+
// Find the shade closest to the original for DEFAULT
|
|
72
|
+
const [, , originalL] = hexToHsl(hex);
|
|
73
|
+
let closestShade = '500';
|
|
74
|
+
let closestDiff = Infinity;
|
|
75
|
+
for (const [shade, lightness] of Object.entries(SHADE_LIGHTNESS)) {
|
|
76
|
+
const diff = Math.abs(lightness - originalL);
|
|
77
|
+
if (diff < closestDiff) {
|
|
78
|
+
closestDiff = diff;
|
|
79
|
+
closestShade = shade;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const defaultHex = hslToHex(h, s, SHADE_LIGHTNESS[closestShade]);
|
|
83
|
+
entries.push(` DEFAULT: '${defaultHex}',`);
|
|
84
|
+
return `{\n${entries.join('\n')}\n }`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared React Components Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates: ViewToggle.tsx, SearchBar.tsx, FilterBar.tsx
|
|
5
|
+
* These are framework-agnostic React components reusable across toolkits.
|
|
6
|
+
*/
|
|
7
|
+
import type { FactoryFile } from '../types.js';
|
|
8
|
+
export declare function generateViewToggle(): FactoryFile;
|
|
9
|
+
export declare function generateSearchBar(): FactoryFile;
|
|
10
|
+
export declare function generateFilterBar(): FactoryFile;
|
|
11
|
+
/** Generate all shared components (ViewToggle, SearchBar, FilterBar). */
|
|
12
|
+
export declare function generateSharedComponents(): FactoryFile[];
|