339 lines
12 KiB
JavaScript
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!');
|