Files
productcatalogue_frontend/scripts/scaffold.cjs
T

339 lines
12 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const features = [
{ name: 'attribute-groups', Name: 'AttributeGroup' },
{ name: 'assets', Name: 'Asset' },
{ name: 'asset-types', Name: 'AssetType' },
{ name: 'asset-families', Name: 'AssetFamily' },
{ name: 'imports', Name: 'Import' },
{ name: 'workflow', Name: 'Workflow' },
{ name: 'channels', Name: 'Channel' },
{ name: 'integrations', Name: 'Integration' },
{ name: 'users', Name: 'User' },
{ name: 'reports', Name: 'Report' },
{ name: 'settings', Name: 'Setting' },
];
const basePath = path.join(__dirname, '../src/features');
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
const camelCase = (s) => s.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
features.forEach(({ name, Name }) => {
const camelName = camelCase(name);
const featurePath = path.join(basePath, name);
if (!fs.existsSync(featurePath)) fs.mkdirSync(featurePath, { recursive: true });
['api', 'hook', 'pages', 'routes', 'services', 'types', 'validation'].forEach(dir => {
const dirPath = path.join(featurePath, dir);
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
});
// types
fs.writeFileSync(path.join(featurePath, `types/${name}.types.ts`), `export interface ${Name} {
id: string;
name: string;
status: 'active' | 'inactive';
createdAt: string;
}
export type ${Name}CreateRequest = Omit<${Name}, 'id' | 'createdAt'>;
export type ${Name}UpdateRequest = Partial<${Name}CreateRequest>;
`);
// validation
fs.writeFileSync(path.join(featurePath, `validation/${name}.schema.ts`), `import type { ${Name}CreateRequest } from '../types/${name}.types';
export const validate${Name} = (data: Partial<${Name}CreateRequest>): Record<string, string> => {
const errors: Record<string, string> = {};
if (!data.name?.trim()) errors.name = '${Name} name is required';
return errors;
};
`);
// service
fs.writeFileSync(path.join(featurePath, `services/${name}.service.ts`), `import type { ${Name}, ${Name}CreateRequest, ${Name}UpdateRequest } from '../types/${name}.types';
const STORAGE_KEY = 'pim_${name.replace('-', '_')}';
const getStored = (): ${Name}[] => {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return [];
return JSON.parse(stored);
};
const setStored = (items: ${Name}[]) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
};
export const ${camelName}Service = {
getAll: async (): Promise<${Name}[]> => {
return new Promise((resolve) => setTimeout(() => resolve(getStored()), 300));
},
getById: async (id: string): Promise<${Name} | undefined> => {
return new Promise((resolve) => setTimeout(() => resolve(getStored().find(p => p.id === id)), 200));
},
create: async (req: ${Name}CreateRequest): Promise<${Name}> => {
return new Promise((resolve) => {
setTimeout(() => {
const list = getStored();
const newItem: ${Name} = { ...req, id: String(Date.now()), createdAt: new Date().toISOString() };
list.push(newItem);
setStored(list);
resolve(newItem);
}, 300);
});
},
update: async (id: string, req: ${Name}UpdateRequest): Promise<${Name}> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const list = getStored();
const index = list.findIndex(p => p.id === id);
if (index === -1) { reject(new Error('Not found')); return; }
const updated = { ...list[index], ...req };
list[index] = updated;
setStored(list);
resolve(updated);
}, 300);
});
},
delete: async (id: string): Promise<boolean> => {
return new Promise((resolve) => {
setTimeout(() => {
const list = getStored().filter(p => p.id !== id);
setStored(list);
resolve(true);
}, 300);
});
},
};
`);
// api
fs.writeFileSync(path.join(featurePath, `api/${name}.api.ts`), `import { ${camelName}Service } from '../services/${name}.service';
export const ${camelName}Api = ${camelName}Service;
`);
// hook
fs.writeFileSync(path.join(featurePath, `hook/use${Name}.ts`), `import { useState, useCallback } from 'react';
import { ${camelName}Service } from '../services/${name}.service';
import type { ${Name}, ${Name}CreateRequest, ${Name}UpdateRequest } from '../types/${name}.types';
import { toast } from 'react-toastify';
export const use${Name} = () => {
const [items, setItems] = useState<${Name}[]>([]);
const [loading, setLoading] = useState(false);
const fetchItems = useCallback(async () => {
setLoading(true);
try {
const data = await ${camelName}Service.getAll();
setItems(data);
} catch (err: any) {
toast.error(err.message || 'Failed to fetch items');
} finally {
setLoading(false);
}
}, []);
const createItem = useCallback(async (req: ${Name}CreateRequest) => {
setLoading(true);
try {
const created = await ${camelName}Service.create(req);
setItems((prev) => [...prev, created]);
toast.success('${Name} created successfully!');
return created;
} catch (err: any) {
toast.error(err.message || 'Failed to create item');
throw err;
} finally {
setLoading(false);
}
}, []);
const updateItem = useCallback(async (id: string, req: ${Name}UpdateRequest) => {
setLoading(true);
try {
const updated = await ${camelName}Service.update(id, req);
setItems((prev) => prev.map((p) => (p.id === id ? updated : p)));
toast.success('${Name} updated successfully!');
return updated;
} catch (err: any) {
toast.error(err.message || 'Failed to update item');
throw err;
} finally {
setLoading(false);
}
}, []);
const deleteItem = useCallback(async (id: string) => {
setLoading(true);
try {
await ${camelName}Service.delete(id);
setItems((prev) => prev.filter((p) => p.id !== id));
toast.success('${Name} deleted successfully!');
} catch (err: any) {
toast.error(err.message || 'Failed to delete item');
throw err;
} finally {
setLoading(false);
}
}, []);
return { items, loading, fetchItems, createItem, updateItem, deleteItem };
};
`);
// pages - List
fs.writeFileSync(path.join(featurePath, `pages/${Name}List.tsx`), `import { useState, useEffect } from "react";
import { Plus, Edit2, Trash2 } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Button } from "../../../components/customs/Button";
import { Table } from "../../../components/customs/Table";
import { use${Name} } from "../hook/use${Name}";
import type { ${Name} } from "../types/${name}.types";
export default function ${Name}List() {
const navigate = useNavigate();
const { items, fetchItems, deleteItem } = use${Name}();
const [selected, setSelected] = useState<Set<string>>(new Set());
useEffect(() => {
fetchItems();
}, [fetchItems]);
const handleSelect = (ids: Set<string>) => setSelected(ids);
const handleDelete = async (item: ${Name}) => {
if (confirm('Delete this item?')) {
await deleteItem(item.id);
}
};
const columns = [
{ key: "name", header: "Name", sortable: true },
{ key: "status", header: "Status", sortable: true },
{ key: "createdAt", header: "Created At", sortable: true, render: (val: string) => new Date(val).toLocaleDateString() },
];
const actions = (item: ${Name}) => (
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" icon={<Edit2 className="w-4 h-4" />} onClick={() => navigate(item.id + "/edit")} />
<Button variant="ghost" size="sm" icon={<Trash2 className="w-4 h-4 text-red-500" />} onClick={() => handleDelete(item)} />
</div>
);
return (
<PageWrapper>
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">${Name}s</h1>
<p className="text-sm text-gray-500">Manage your ${name.replace('-', ' ')}</p>
</div>
<Button variant="primary" icon={<Plus className="w-4 h-4" />} onClick={() => navigate("new")}>
Create ${Name}
</Button>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
<Table data={items} columns={columns} selectable onSelectionChange={handleSelect} actions={actions} />
</div>
</PageWrapper>
);
}
`);
// pages - New
fs.writeFileSync(path.join(featurePath, `pages/New${Name}.tsx`), `import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { PageWrapper } from "../../../components/layouts/PageWrapper";
import { Button } from "../../../components/customs/Button";
import { use${Name} } from "../hook/use${Name}";
import { validate${Name} } from "../validation/${name}.schema";
import { ${camelName}Service } from "../services/${name}.service";
import type { ${Name}CreateRequest } from "../types/${name}.types";
export default function New${Name}() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const isEdit = Boolean(id);
const { createItem, updateItem } = use${Name}();
const [form, setForm] = useState<Partial<${Name}CreateRequest>>({ name: "", status: "active" });
const [errors, setErrors] = useState<Record<string, string>>({});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (isEdit && id) {
${camelName}Service.getById(id).then(item => {
if (item) setForm({ name: item.name, status: item.status });
});
}
}, [isEdit, id]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const validationErrors = validate${Name}(form);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setSaving(true);
try {
if (isEdit && id) {
await updateItem(id, form as any);
} else {
await createItem(form as ${Name}CreateRequest);
}
navigate("..");
} catch {
// toast handled
} finally {
setSaving(false);
}
};
return (
<PageWrapper>
<div className="max-w-2xl mx-auto">
<h1 className="text-2xl font-bold text-gray-900 mb-6">{isEdit ? 'Edit' : 'Create'} ${Name}</h1>
<form onSubmit={handleSubmit} className="bg-white border border-gray-200 rounded-xl p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input type="text" value={form.name} onChange={e => setForm({ ...form, name: e.target.value })}
className="w-full border border-gray-300 rounded-lg px-3 py-2" />
{errors.name && <p className="text-red-500 text-xs mt-1">{errors.name}</p>}
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="outline" type="button" onClick={() => navigate("..")}>Cancel</Button>
<Button variant="primary" type="submit" loading={saving}>Save</Button>
</div>
</form>
</div>
</PageWrapper>
);
}
`);
// routes
fs.writeFileSync(path.join(featurePath, `routes/${name}.routes.tsx`), `import { Routes, Route } from 'react-router-dom';
import ${Name}List from '../pages/${Name}List';
import New${Name} from '../pages/New${Name}';
export const ${Name}Routes = () => (
<Routes>
<Route index element={<${Name}List />} />
<Route path="new" element={<New${Name} />} />
<Route path=":id/edit" element={<New${Name} />} />
</Routes>
);
`);
// index
fs.writeFileSync(path.join(featurePath, `index.ts`), `export * from './types/${name}.types';
export * from './services/${name}.service';
export * from './hook/use${Name}';
export * from './routes/${name}.routes';
`);
});
console.log('Scaffolding complete!');