stackloom-cli 1.0.0

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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/cli.js +306 -0
  4. package/branding.json +8 -0
  5. package/package.json +72 -0
  6. package/src/__tests__/cli-smoke.test.js +46 -0
  7. package/src/blueprint/__tests__/blueprint.test.js +116 -0
  8. package/src/blueprint/blueprint.js +181 -0
  9. package/src/blueprint/default.blueprint.json +78 -0
  10. package/src/blueprint/index.js +10 -0
  11. package/src/blueprint/loader.js +101 -0
  12. package/src/blueprint/schema-kit.js +161 -0
  13. package/src/blueprint/schema.js +78 -0
  14. package/src/branding/__tests__/branding.test.js +49 -0
  15. package/src/branding/index.js +48 -0
  16. package/src/commands/__tests__/commands.test.js +83 -0
  17. package/src/commands/check.js +71 -0
  18. package/src/commands/cleanup.js +347 -0
  19. package/src/commands/customize.js +263 -0
  20. package/src/commands/doctor.js +84 -0
  21. package/src/commands/env.js +75 -0
  22. package/src/commands/finalize.js +68 -0
  23. package/src/commands/generate/ci-cd.js +378 -0
  24. package/src/commands/generate/deploy-advanced.js +253 -0
  25. package/src/commands/generate/deploy.js +99 -0
  26. package/src/commands/generate/env.template.js +221 -0
  27. package/src/commands/generate/index.js +7 -0
  28. package/src/commands/generate/module.js +836 -0
  29. package/src/commands/generate/page.js +1415 -0
  30. package/src/commands/generate/test-scaffold.js +279 -0
  31. package/src/commands/generate/theme.js +67 -0
  32. package/src/commands/generate-resource.js +133 -0
  33. package/src/commands/index.js +9 -0
  34. package/src/commands/init.js +350 -0
  35. package/src/commands/make/resource.js +298 -0
  36. package/src/commands/preset.js +57 -0
  37. package/src/commands/remove.js +170 -0
  38. package/src/commands/rename.js +54 -0
  39. package/src/commands/rollback.js +90 -0
  40. package/src/commands/wizard.js +303 -0
  41. package/src/core/__tests__/generator.test.js +67 -0
  42. package/src/core/__tests__/marker-strategy.test.js +57 -0
  43. package/src/core/__tests__/resource-definition.test.js +32 -0
  44. package/src/core/generator.js +542 -0
  45. package/src/core/marker-strategy.js +138 -0
  46. package/src/core/resource-definition.js +346 -0
  47. package/src/core/state-tracker.js +67 -0
  48. package/src/core/template-loader.js +163 -0
  49. package/src/engine/__tests__/engine.test.js +306 -0
  50. package/src/engine/index.js +21 -0
  51. package/src/engine/injector.js +198 -0
  52. package/src/engine/pipeline.js +138 -0
  53. package/src/engine/transaction.js +105 -0
  54. package/src/engine/validator.js +190 -0
  55. package/src/index.js +4 -0
  56. package/src/recipes/__tests__/recipe.test.js +128 -0
  57. package/src/recipes/builtin/module.json +22 -0
  58. package/src/recipes/builtin/page.json +21 -0
  59. package/src/recipes/builtin/resource.json +35 -0
  60. package/src/recipes/condition.js +147 -0
  61. package/src/recipes/index.js +11 -0
  62. package/src/recipes/loader.js +95 -0
  63. package/src/recipes/recipe.js +89 -0
  64. package/src/recipes/schema.js +47 -0
  65. package/src/schemas/__tests__/schemas.test.js +67 -0
  66. package/src/schemas/index.js +18 -0
  67. package/src/schemas/options.js +38 -0
  68. package/src/schemas/resource.js +112 -0
  69. package/src/services/__tests__/reporter.test.js +98 -0
  70. package/src/services/clock.js +31 -0
  71. package/src/services/index.js +43 -0
  72. package/src/services/reporter.js +136 -0
  73. package/src/templates/resource/api.js.ejs +39 -0
  74. package/src/templates/resource/components/form.jsx.ejs +81 -0
  75. package/src/templates/resource/components/table.jsx.ejs +68 -0
  76. package/src/templates/resource/controller.js.ejs +154 -0
  77. package/src/templates/resource/hooks.js.ejs +46 -0
  78. package/src/templates/resource/model.js.ejs +64 -0
  79. package/src/templates/resource/page-detail.jsx.ejs +55 -0
  80. package/src/templates/resource/page-form.jsx.ejs +30 -0
  81. package/src/templates/resource/page-inline.jsx.ejs +74 -0
  82. package/src/templates/resource/page-modal.jsx.ejs +98 -0
  83. package/src/templates/resource/page-page.jsx.ejs +99 -0
  84. package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
  85. package/src/templates/resource/routes.js.ejs +35 -0
  86. package/src/templates/resource/service.js.ejs +132 -0
  87. package/src/templates/resource/test.ejs +71 -0
  88. package/src/templates/resource/types.ts.ejs +17 -0
  89. package/src/templates/resource/validator.js.ejs +26 -0
  90. package/src/templates/snippets/lazy-import.ejs +1 -0
  91. package/src/templates/snippets/nav-entry.ejs +1 -0
  92. package/src/templates/snippets/route-entry.ejs +5 -0
  93. package/src/templates/snippets/route-mount.ejs +1 -0
  94. package/src/utils/fieldValidators.js +371 -0
  95. package/src/utils/logging/logger.js +47 -0
  96. package/src/utils/namingUtils.js +38 -0
  97. package/src/utils/sanitize.js +200 -0
@@ -0,0 +1,46 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import api from '../api/<%= resource.kebabName %>.api';
3
+
4
+ /**
5
+ * use<%= resource.pascalName %> hook
6
+ * Handles data fetching and state for <%= resource.name %> resource
7
+ */
8
+ export const use<%= resource.pascalName %> = (id = null) => {
9
+ const [data, setData] = useState(id ? null : []);
10
+ const [loading, setLoading] = useState(false);
11
+ const [error, setError] = useState(null);
12
+
13
+ const fetchAll = useCallback(async (params) => {
14
+ setLoading(true);
15
+ try {
16
+ const res = await api.getAll(params);
17
+ setData(res.data);
18
+ } catch (err) {
19
+ setError(err);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ }, []);
24
+
25
+ const fetchOne = useCallback(async (uid) => {
26
+ setLoading(true);
27
+ try {
28
+ const res = await api.getById(uid);
29
+ setData(res.data);
30
+ } catch (err) {
31
+ setError(err);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ if (id) {
39
+ fetchOne(id);
40
+ } else {
41
+ fetchAll();
42
+ }
43
+ }, [id, fetchOne, fetchAll]);
44
+
45
+ return { data, loading, error, refresh: id ? () => fetchOne(id) : fetchAll };
46
+ };
@@ -0,0 +1,64 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ // ── Mongoose Field Definitions ──────────────────────────────────────────────
4
+ <% const fields = resource.fields; %>
5
+ const <%= resource.pascalName %>Schema = new mongoose.Schema(
6
+ {
7
+ <% fields.forEach(function(field) { -%>
8
+ <%- field.name %>: { <%- field.mongooseDef %> },
9
+ <% }); -%>
10
+ },
11
+ {
12
+ timestamps: true,
13
+ toJSON: {
14
+ virtuals: true,
15
+ transform: (doc, ret) => {
16
+ delete ret._id;
17
+ delete ret.__v;
18
+ return ret;
19
+ },
20
+ },
21
+ toObject: { virtuals: true },
22
+ }
23
+ );
24
+
25
+ // ── Indexes ── (text fields indexed for search; unique fields are indexed inline)
26
+ <% fields.forEach(function(field) { -%>
27
+ <% if (field.type === 'text' || field.type === 'richtext') { -%>
28
+ <%= resource.pascalName %>Schema.index({ <%- field.name %>: 'text' });
29
+ <% } -%>
30
+ <% }); -%>
31
+
32
+ <% if (resource.features.softDelete) { %>
33
+ // Soft delete plugin (adds deletedAt, isDeleted virtual)
34
+ <%= resource.pascalName %>Schema.plugin(require('mongoose-delete'), { deletedAt: true });
35
+ <% } %>
36
+
37
+ <% if (resource.relations && resource.relations.hasMany && resource.relations.hasMany.length > 0) { %>
38
+ // Virtual populate for hasMany relations
39
+ <% resource.relations.hasMany.forEach(function(rel) { %>
40
+ <%= resource.pascalName %>Schema.virtual('<%- rel.field %>', {
41
+ ref: '<%- rel.model %>',
42
+ localField: '_id',
43
+ foreignField: '<%- rel.foreignKey || (rel.field + 'Id') %>',
44
+ justOne: false
45
+ });
46
+ <% }); %>
47
+ <% } %>
48
+
49
+ // ═══════════════════════════════════════════════════════════════════════════
50
+ // ✎ CUSTOM CODE ZONE — YOUR CODE HERE
51
+ // Add virtuals, methods, statics, plugins below:
52
+ //
53
+ // <%= resource.pascalName %>Schema.virtual('fullName').get(function() { ... });
54
+ // <%= resource.pascalName %>Schema.methods.comparePassword = function(candidate) { ... };
55
+ // <%= resource.pascalName %>Schema.statics.findActive = function() { ... };
56
+ // <%= resource.pascalName %>Schema.pre('save', function(next) { ... });
57
+ //
58
+ // ────────────────────────────────────────────────────────────────────────────
59
+
60
+ module.exports = mongoose.model('<%= resource.pascalName %>', <%= resource.pascalName %>Schema);
61
+
62
+ // ═══════════════════════════════════════════════════════════════════════════
63
+ // END AUTO-GENERATED
64
+ // ═══════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import { use<%= resource.pascalName %> } from "@/hooks/use<%= resource.pascalName %>";
4
+ import { Button } from "@/components/ui/button";
5
+ import { ArrowLeft, Edit } from "lucide-react";
6
+ import { PageWrapper } from "@/components/layout/PageWrapper";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+
9
+ export default function <%= resource.pascalName %>DetailPage() {
10
+ const { id } = useParams();
11
+ const navigate = useNavigate();
12
+ const { data: item, loading, error } = use<%= resource.pascalName %>(id);
13
+
14
+ if (loading) return <div>Loading...</div>;
15
+ if (error) return <div>Error loading <%= resource.name.toLowerCase() %></div>;
16
+ if (!item) return <div><%= resource.name %> not found</div>;
17
+
18
+ return (
19
+ <PageWrapper
20
+ title={`<%= resource.name %> Details`}
21
+ actions={
22
+ <div className="flex gap-2">
23
+ <Button variant="outline" onClick={() => navigate(-1)}>
24
+ <ArrowLeft className="mr-2 h-4 w-4" /> Back
25
+ </Button>
26
+ <Button onClick={() => navigate(`/admin/<%= resource.kebabName %>/edit/${id}`)}>
27
+ <Edit className="mr-2 h-4 w-4" /> Edit
28
+ </Button>
29
+ </div>
30
+ }
31
+ >
32
+ <div className="grid gap-4 md:grid-cols-2">
33
+ <Card>
34
+ <CardHeader>
35
+ <CardTitle>Information</CardTitle>
36
+ </CardHeader>
37
+ <CardContent className="space-y-2">
38
+ <% resource.fields.forEach(function(field) { %>
39
+ <div className="flex justify-between border-b pb-2">
40
+ <span className="font-medium text-muted-foreground"><%- field.name.charAt(0).toUpperCase() + field.name.slice(1) %></span>
41
+ <span>{<% if (field.type === 'boolean') { %>
42
+ item.<%- field.name %> ? 'Yes' : 'No'
43
+ <% } else if (field.type === 'date' || field.type === 'datetime') { %>
44
+ new Date(item.<%- field.name %>).toLocaleString()
45
+ <% } else { %>
46
+ item.<%- field.name %>
47
+ <% } %>}</span>
48
+ </div>
49
+ <% }); %>
50
+ </CardContent>
51
+ </Card>
52
+ </div>
53
+ </PageWrapper>
54
+ );
55
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ } from "@/components/ui/dialog";
9
+ import { <%= resource.pascalName %>Form } from "@/components/forms/<%= resource.pascalName %>Form";
10
+
11
+ export const FormModal = ({ open, onOpenChange, initialData, onSubmit, isLoading }) => {
12
+ return (
13
+ <Dialog open={open} onOpenChange={onOpenChange}>
14
+ <DialogContent className="sm:max-w-[425px]">
15
+ <DialogHeader>
16
+ <DialogTitle>{initialData ? 'Edit' : 'Create'} <%= resource.name %></DialogTitle>
17
+ <DialogDescription>
18
+ {initialData ? 'Update the details below.' : 'Fill in the details for the new <%= resource.name.toLowerCase() %>.'}
19
+ </DialogDescription>
20
+ </DialogHeader>
21
+ <<%= resource.pascalName %>Form
22
+ initialData={initialData}
23
+ onSubmit={onSubmit}
24
+ onCancel={() => onOpenChange(false)}
25
+ isLoading={isLoading}
26
+ />
27
+ </DialogContent>
28
+ </Dialog>
29
+ );
30
+ };
@@ -0,0 +1,74 @@
1
+ import React, { useState } from 'react';
2
+ import { use<%= resource.pascalName %> } from "@/hooks/use<%= resource.pascalName %>";
3
+ import { <%= resource.pascalName %>Table } from "@/components/tables/<%= resource.pascalName %>Table";
4
+ import { <%= resource.pascalName %>Form } from "@/components/forms/<%= resource.pascalName %>Form";
5
+ import { Button } from "@/components/ui/button";
6
+ import { RefreshCcw } from "lucide-react";
7
+ import { PageWrapper } from "@/components/layout/PageWrapper";
8
+ import api from "@/api/<%= resource.kebabName %>.api";
9
+ import { toast } from "sonner";
10
+
11
+ // form-mode: inline — the create/edit form is always visible above the table.
12
+ export default function <%= resource.pascalName %>ListPage() {
13
+ const { data, loading, refresh } = use<%= resource.pascalName %>();
14
+ const [editingItem, setEditingItem] = useState(null);
15
+
16
+ const handleEdit = (item) => setEditingItem(item);
17
+
18
+ const handleDelete = async (item) => {
19
+ if (!confirm("Are you sure you want to delete this <%= resource.name.toLowerCase() %>?")) return;
20
+ try {
21
+ await api.delete(item._id);
22
+ toast.success("<%= resource.name %> deleted successfully");
23
+ refresh();
24
+ } catch (err) {
25
+ toast.error("Failed to delete <%= resource.name.toLowerCase() %>");
26
+ }
27
+ };
28
+
29
+ const handleSubmit = async (formData) => {
30
+ try {
31
+ if (editingItem) {
32
+ await api.update(editingItem._id, formData);
33
+ toast.success("<%= resource.name %> updated successfully");
34
+ } else {
35
+ await api.create(formData);
36
+ toast.success("<%= resource.name %> created successfully");
37
+ }
38
+ setEditingItem(null);
39
+ refresh();
40
+ } catch (err) {
41
+ toast.error(editingItem ? "Failed to update <%= resource.name.toLowerCase() %>" : "Failed to create <%= resource.name.toLowerCase() %>");
42
+ }
43
+ };
44
+
45
+ return (
46
+ <PageWrapper
47
+ title="<%= resource.pluralPascal %>"
48
+ subtitle="Manage your <%= resource.pluralPascal.toLowerCase() %>"
49
+ actions={
50
+ <Button variant="outline" size="icon" onClick={() => refresh()} disabled={loading}>
51
+ <RefreshCcw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
52
+ </Button>
53
+ }
54
+ >
55
+ <div className="space-y-6">
56
+ <section className="rounded-lg border bg-card p-6">
57
+ <h2 className="mb-4 text-lg font-medium">
58
+ {editingItem ? "Edit <%= resource.name %>" : "Create <%= resource.name %>"}
59
+ </h2>
60
+ <<%= resource.pascalName %>Form
61
+ initialData={editingItem}
62
+ onSubmit={handleSubmit}
63
+ onCancel={editingItem ? () => setEditingItem(null) : undefined}
64
+ isLoading={loading}
65
+ />
66
+ </section>
67
+
68
+ <section>
69
+ <<%= resource.pascalName %>Table data={data} onEdit={handleEdit} onDelete={handleDelete} />
70
+ </section>
71
+ </div>
72
+ </PageWrapper>
73
+ );
74
+ }
@@ -0,0 +1,98 @@
1
+ import React, { useState } from 'react';
2
+ import { use<%= resource.pascalName %> } from "@/hooks/use<%= resource.pascalName %>";
3
+ import { <%= resource.pascalName %>Table } from "@/components/tables/<%= resource.pascalName %>Table";
4
+ import { <%= resource.pascalName %>Form } from "@/components/forms/<%= resource.pascalName %>Form";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogDescription,
12
+ } from "@/components/ui/dialog";
13
+ import { Plus, RefreshCcw } from "lucide-react";
14
+ import { PageWrapper } from "@/components/layout/PageWrapper";
15
+ import api from "@/api/<%= resource.kebabName %>.api";
16
+ import { toast } from "sonner";
17
+
18
+ // form-mode: modal — create/edit form is mounted inside a Dialog.
19
+ export default function <%= resource.pascalName %>ListPage() {
20
+ const { data, loading, refresh } = use<%= resource.pascalName %>();
21
+ const [formOpen, setFormOpen] = useState(false);
22
+ const [editingItem, setEditingItem] = useState(null);
23
+
24
+ const handleCreate = () => {
25
+ setEditingItem(null);
26
+ setFormOpen(true);
27
+ };
28
+
29
+ const handleEdit = (item) => {
30
+ setEditingItem(item);
31
+ setFormOpen(true);
32
+ };
33
+
34
+ const handleDelete = async (item) => {
35
+ if (!confirm("Are you sure you want to delete this <%= resource.name.toLowerCase() %>?")) return;
36
+ try {
37
+ await api.delete(item._id);
38
+ toast.success("<%= resource.name %> deleted successfully");
39
+ refresh();
40
+ } catch (err) {
41
+ toast.error("Failed to delete <%= resource.name.toLowerCase() %>");
42
+ }
43
+ };
44
+
45
+ const handleSubmit = async (formData) => {
46
+ try {
47
+ if (editingItem) {
48
+ await api.update(editingItem._id, formData);
49
+ toast.success("<%= resource.name %> updated successfully");
50
+ } else {
51
+ await api.create(formData);
52
+ toast.success("<%= resource.name %> created successfully");
53
+ }
54
+ setFormOpen(false);
55
+ refresh();
56
+ } catch (err) {
57
+ toast.error(editingItem ? "Failed to update <%= resource.name.toLowerCase() %>" : "Failed to create <%= resource.name.toLowerCase() %>");
58
+ }
59
+ };
60
+
61
+ return (
62
+ <PageWrapper
63
+ title="<%= resource.pluralPascal %>"
64
+ subtitle="Manage your <%= resource.pluralPascal.toLowerCase() %>"
65
+ actions={
66
+ <div className="flex gap-2">
67
+ <Button variant="outline" size="icon" onClick={() => refresh()} disabled={loading}>
68
+ <RefreshCcw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
69
+ </Button>
70
+ <Button onClick={handleCreate}>
71
+ <Plus className="mr-2 h-4 w-4" /> Add <%= resource.name %>
72
+ </Button>
73
+ </div>
74
+ }
75
+ >
76
+ <div className="space-y-4">
77
+ <<%= resource.pascalName %>Table data={data} onEdit={handleEdit} onDelete={handleDelete} />
78
+ </div>
79
+
80
+ <Dialog open={formOpen} onOpenChange={setFormOpen}>
81
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
82
+ <DialogHeader>
83
+ <DialogTitle>{editingItem ? "Edit <%= resource.name %>" : "Create <%= resource.name %>"}</DialogTitle>
84
+ <DialogDescription>
85
+ {editingItem ? "Update the details below." : "Fill in the details to create a new <%= resource.name.toLowerCase() %>."}
86
+ </DialogDescription>
87
+ </DialogHeader>
88
+ <<%= resource.pascalName %>Form
89
+ initialData={editingItem}
90
+ onSubmit={handleSubmit}
91
+ onCancel={() => setFormOpen(false)}
92
+ isLoading={loading}
93
+ />
94
+ </DialogContent>
95
+ </Dialog>
96
+ </PageWrapper>
97
+ );
98
+ }
@@ -0,0 +1,99 @@
1
+ import React, { useState } from 'react';
2
+ import { use<%= resource.pascalName %> } from "@/hooks/use<%= resource.pascalName %>";
3
+ import { <%= resource.pascalName %>Table } from "@/components/tables/<%= resource.pascalName %>Table";
4
+ import { <%= resource.pascalName %>Form } from "@/components/forms/<%= resource.pascalName %>Form";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Plus, RefreshCcw } from "lucide-react";
7
+ import { PageWrapper } from "@/components/layout/PageWrapper";
8
+ import api from "@/api/<%= resource.kebabName %>.api";
9
+ import { toast } from "sonner";
10
+
11
+ // form-mode: page — the create/edit form takes over the page as a dedicated
12
+ // section. A matching FormPage.jsx is also generated for routed create/edit.
13
+ export default function <%= resource.pascalName %>ListPage() {
14
+ const { data, loading, refresh } = use<%= resource.pascalName %>();
15
+ const [formOpen, setFormOpen] = useState(false);
16
+ const [editingItem, setEditingItem] = useState(null);
17
+
18
+ const handleCreate = () => {
19
+ setEditingItem(null);
20
+ setFormOpen(true);
21
+ };
22
+
23
+ const handleEdit = (item) => {
24
+ setEditingItem(item);
25
+ setFormOpen(true);
26
+ };
27
+
28
+ const handleClose = () => {
29
+ setFormOpen(false);
30
+ setEditingItem(null);
31
+ };
32
+
33
+ const handleDelete = async (item) => {
34
+ if (!confirm("Are you sure you want to delete this <%= resource.name.toLowerCase() %>?")) return;
35
+ try {
36
+ await api.delete(item._id);
37
+ toast.success("<%= resource.name %> deleted successfully");
38
+ refresh();
39
+ } catch (err) {
40
+ toast.error("Failed to delete <%= resource.name.toLowerCase() %>");
41
+ }
42
+ };
43
+
44
+ const handleSubmit = async (formData) => {
45
+ try {
46
+ if (editingItem) {
47
+ await api.update(editingItem._id, formData);
48
+ toast.success("<%= resource.name %> updated successfully");
49
+ } else {
50
+ await api.create(formData);
51
+ toast.success("<%= resource.name %> created successfully");
52
+ }
53
+ handleClose();
54
+ refresh();
55
+ } catch (err) {
56
+ toast.error(editingItem ? "Failed to update <%= resource.name.toLowerCase() %>" : "Failed to create <%= resource.name.toLowerCase() %>");
57
+ }
58
+ };
59
+
60
+ if (formOpen) {
61
+ return (
62
+ <PageWrapper
63
+ title={editingItem ? "Edit <%= resource.name %>" : "Create <%= resource.name %>"}
64
+ subtitle="Fill in the details below"
65
+ actions={<Button variant="outline" onClick={handleClose}>Back to list</Button>}
66
+ >
67
+ <section className="max-w-2xl rounded-lg border bg-card p-6">
68
+ <<%= resource.pascalName %>Form
69
+ initialData={editingItem}
70
+ onSubmit={handleSubmit}
71
+ onCancel={handleClose}
72
+ isLoading={loading}
73
+ />
74
+ </section>
75
+ </PageWrapper>
76
+ );
77
+ }
78
+
79
+ return (
80
+ <PageWrapper
81
+ title="<%= resource.pluralPascal %>"
82
+ subtitle="Manage your <%= resource.pluralPascal.toLowerCase() %>"
83
+ actions={
84
+ <div className="flex gap-2">
85
+ <Button variant="outline" size="icon" onClick={() => refresh()} disabled={loading}>
86
+ <RefreshCcw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
87
+ </Button>
88
+ <Button onClick={handleCreate}>
89
+ <Plus className="mr-2 h-4 w-4" /> Add <%= resource.name %>
90
+ </Button>
91
+ </div>
92
+ }
93
+ >
94
+ <div className="space-y-4">
95
+ <<%= resource.pascalName %>Table data={data} onEdit={handleEdit} onDelete={handleDelete} />
96
+ </div>
97
+ </PageWrapper>
98
+ );
99
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useState } from 'react';
2
+ import { use<%= resource.pascalName %> } from "@/hooks/use<%= resource.pascalName %>";
3
+ import { <%= resource.pascalName %>Table } from "@/components/tables/<%= resource.pascalName %>Table";
4
+ import { <%= resource.pascalName %>Form } from "@/components/forms/<%= resource.pascalName %>Form";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetHeader,
10
+ SheetTitle,
11
+ SheetDescription,
12
+ } from "@/components/ui/sheet";
13
+ import { Plus, RefreshCcw } from "lucide-react";
14
+ import { PageWrapper } from "@/components/layout/PageWrapper";
15
+ import api from "@/api/<%= resource.kebabName %>.api";
16
+ import { toast } from "sonner";
17
+
18
+ // form-mode: sidepanel — create/edit form is mounted inside a slide-over Sheet.
19
+ export default function <%= resource.pascalName %>ListPage() {
20
+ const { data, loading, refresh } = use<%= resource.pascalName %>();
21
+ const [formOpen, setFormOpen] = useState(false);
22
+ const [editingItem, setEditingItem] = useState(null);
23
+
24
+ const handleCreate = () => {
25
+ setEditingItem(null);
26
+ setFormOpen(true);
27
+ };
28
+
29
+ const handleEdit = (item) => {
30
+ setEditingItem(item);
31
+ setFormOpen(true);
32
+ };
33
+
34
+ const handleDelete = async (item) => {
35
+ if (!confirm("Are you sure you want to delete this <%= resource.name.toLowerCase() %>?")) return;
36
+ try {
37
+ await api.delete(item._id);
38
+ toast.success("<%= resource.name %> deleted successfully");
39
+ refresh();
40
+ } catch (err) {
41
+ toast.error("Failed to delete <%= resource.name.toLowerCase() %>");
42
+ }
43
+ };
44
+
45
+ const handleSubmit = async (formData) => {
46
+ try {
47
+ if (editingItem) {
48
+ await api.update(editingItem._id, formData);
49
+ toast.success("<%= resource.name %> updated successfully");
50
+ } else {
51
+ await api.create(formData);
52
+ toast.success("<%= resource.name %> created successfully");
53
+ }
54
+ setFormOpen(false);
55
+ refresh();
56
+ } catch (err) {
57
+ toast.error(editingItem ? "Failed to update <%= resource.name.toLowerCase() %>" : "Failed to create <%= resource.name.toLowerCase() %>");
58
+ }
59
+ };
60
+
61
+ return (
62
+ <PageWrapper
63
+ title="<%= resource.pluralPascal %>"
64
+ subtitle="Manage your <%= resource.pluralPascal.toLowerCase() %>"
65
+ actions={
66
+ <div className="flex gap-2">
67
+ <Button variant="outline" size="icon" onClick={() => refresh()} disabled={loading}>
68
+ <RefreshCcw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
69
+ </Button>
70
+ <Button onClick={handleCreate}>
71
+ <Plus className="mr-2 h-4 w-4" /> Add <%= resource.name %>
72
+ </Button>
73
+ </div>
74
+ }
75
+ >
76
+ <div className="space-y-4">
77
+ <<%= resource.pascalName %>Table data={data} onEdit={handleEdit} onDelete={handleDelete} />
78
+ </div>
79
+
80
+ <Sheet open={formOpen} onOpenChange={setFormOpen}>
81
+ <SheetContent className="overflow-y-auto sm:max-w-lg">
82
+ <SheetHeader>
83
+ <SheetTitle>{editingItem ? "Edit <%= resource.name %>" : "Create <%= resource.name %>"}</SheetTitle>
84
+ <SheetDescription>
85
+ {editingItem ? "Update the details below." : "Fill in the details to create a new <%= resource.name.toLowerCase() %>."}
86
+ </SheetDescription>
87
+ </SheetHeader>
88
+ <div className="mt-4">
89
+ <<%= resource.pascalName %>Form
90
+ initialData={editingItem}
91
+ onSubmit={handleSubmit}
92
+ onCancel={() => setFormOpen(false)}
93
+ isLoading={loading}
94
+ />
95
+ </div>
96
+ </SheetContent>
97
+ </Sheet>
98
+ </PageWrapper>
99
+ );
100
+ }
@@ -0,0 +1,35 @@
1
+ const express = require('express');
2
+ const controller = require('../controllers/<%= resource.name %>.controller');
3
+ const validate = require('../../../middlewares/validate');
4
+ const { authenticateSession } = require('../../../middlewares/auth.middleware');
5
+ const validator = require('../../../utils/validators/<%= resource.name %>.validator');
6
+
7
+ const router = express.Router();
8
+
9
+ // All routes require authentication by default — opt out per-route if needed.
10
+ router.use(authenticateSession);
11
+
12
+ // ── Create ───────────────────────────────────────────────────────────────────
13
+ router.post(
14
+ '/',
15
+ validate(validator.create<%= resource.pascalName %>Schema),
16
+ controller.create
17
+ );
18
+
19
+ // ── List (query params handled in controller/service) ─────────────────────────
20
+ router.get('/', controller.list);
21
+
22
+ // ── Get One ──────────────────────────────────────────────────────────────────
23
+ router.get('/:id', controller.getOne);
24
+
25
+ // ── Update ───────────────────────────────────────────────────────────────────
26
+ router.put(
27
+ '/:id',
28
+ validate(validator.update<%= resource.pascalName %>Schema),
29
+ controller.update
30
+ );
31
+
32
+ // ── Delete ───────────────────────────────────────────────────────────────────
33
+ router.delete('/:id', controller.remove);
34
+
35
+ module.exports = router;