@hypersonic-js/admin 0.2.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/constants.d.ts +6 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +6 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/crud/pagination.d.ts +11 -0
  7. package/dist/crud/pagination.d.ts.map +1 -0
  8. package/dist/crud/pagination.js +31 -0
  9. package/dist/crud/pagination.js.map +1 -0
  10. package/dist/crud/query.d.ts +64 -0
  11. package/dist/crud/query.d.ts.map +1 -0
  12. package/dist/crud/query.js +137 -0
  13. package/dist/crud/query.js.map +1 -0
  14. package/dist/crud/router.d.ts +49 -0
  15. package/dist/crud/router.d.ts.map +1 -0
  16. package/dist/crud/router.js +335 -0
  17. package/dist/crud/router.js.map +1 -0
  18. package/dist/dmmf/fields.d.ts +37 -0
  19. package/dist/dmmf/fields.d.ts.map +1 -0
  20. package/dist/dmmf/fields.js +88 -0
  21. package/dist/dmmf/fields.js.map +1 -0
  22. package/dist/dmmf/parser.d.ts +15 -0
  23. package/dist/dmmf/parser.d.ts.map +1 -0
  24. package/dist/dmmf/parser.js +54 -0
  25. package/dist/dmmf/parser.js.map +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/middleware/auth.d.ts +8 -0
  31. package/dist/middleware/auth.d.ts.map +1 -0
  32. package/dist/middleware/auth.js +21 -0
  33. package/dist/middleware/auth.js.map +1 -0
  34. package/dist/mount.d.ts +4 -0
  35. package/dist/mount.d.ts.map +1 -0
  36. package/dist/mount.js +21 -0
  37. package/dist/mount.js.map +1 -0
  38. package/dist/scaffold/index.d.ts +14 -0
  39. package/dist/scaffold/index.d.ts.map +1 -0
  40. package/dist/scaffold/index.js +35 -0
  41. package/dist/scaffold/index.js.map +1 -0
  42. package/dist/scaffold/templates.d.ts +19 -0
  43. package/dist/scaffold/templates.d.ts.map +1 -0
  44. package/dist/scaffold/templates.js +371 -0
  45. package/dist/scaffold/templates.js.map +1 -0
  46. package/dist/templates/Dashboard.tsx +40 -0
  47. package/dist/templates/ModelForm.tsx +288 -0
  48. package/dist/templates/ModelIndex.tsx +142 -0
  49. package/dist/templates/UserCreate.tsx +189 -0
  50. package/dist/types.d.ts +159 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +60 -0
  55. package/templates/Dashboard.tsx +40 -0
  56. package/templates/ModelForm.tsx +288 -0
  57. package/templates/ModelIndex.tsx +142 -0
  58. package/templates/UserCreate.tsx +189 -0
package/dist/mount.js ADDED
@@ -0,0 +1,21 @@
1
+ import { createAdminAuthMiddleware } from './middleware/auth.js';
2
+ import { createAdminRouter } from './crud/router.js';
3
+ const DEFAULT_HIDDEN_MODELS = ['Session', 'Account', 'Verification', 'JwksKey'];
4
+ const DEFAULT_PREFIX = '/admin';
5
+ export function mountAdmin(app, prisma, options) {
6
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
7
+ const hidden = new Set([
8
+ ...(options.showAuthModels === true ? [] : DEFAULT_HIDDEN_MODELS),
9
+ ...(options.hiddenModels ?? []),
10
+ ]);
11
+ const models = options.meta.filter((m) => !hidden.has(m.name));
12
+ const authMiddleware = createAdminAuthMiddleware(options.auth);
13
+ const router = createAdminRouter(prisma, models, prefix, {
14
+ allMeta: options.meta,
15
+ logger: options.logger,
16
+ auth: options.auth,
17
+ betterAuthUserModel: options.betterAuthUserModel,
18
+ });
19
+ app.use(prefix, authMiddleware, router);
20
+ }
21
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEpD,MAAM,qBAAqB,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,CAAC,CAAA;AAC/E,MAAM,cAAc,GAAG,QAAQ,CAAA;AAE/B,MAAM,UAAU,UAAU,CACxB,GAAgB,EAChB,MAAwB,EACxB,OAAqB;IAErB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;IAE/C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAS;QAC7B,GAAG,CAAC,OAAO,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;KAChC,CAAC,CAAA;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAE9D,MAAM,cAAc,GAAG,yBAAyB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9D,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;QACvD,OAAO,EAAE,OAAO,CAAC,IAAI;QACrB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,mBAAmB,EAAE,OAAO,CAAC,mBAAmB;KACjD,CAAC,CAAA;IAEF,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,CAAA;AACzC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { ScaffoldOptions, ScaffoldResult } from '../types.js';
2
+ /**
3
+ * Copies the admin page components into the user's project.
4
+ * These components are schema-driven and stay in sync with Prisma model changes
5
+ * automatically — no regeneration needed after schema updates.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { scaffoldAdmin } from '@hypersonic-js/admin'
10
+ * await scaffoldAdmin({ targetDir: 'resources/js/Pages', force: false })
11
+ * ```
12
+ */
13
+ export declare function scaffoldAdmin(options?: ScaffoldOptions): Promise<ScaffoldResult>;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scaffold/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAMlE;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAuB1F"}
@@ -0,0 +1,35 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), '../../templates');
5
+ const SCAFFOLD_FILES = ['Dashboard.tsx', 'ModelIndex.tsx', 'ModelForm.tsx', 'UserCreate.tsx'];
6
+ /**
7
+ * Copies the admin page components into the user's project.
8
+ * These components are schema-driven and stay in sync with Prisma model changes
9
+ * automatically — no regeneration needed after schema updates.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { scaffoldAdmin } from '@hypersonic-js/admin'
14
+ * await scaffoldAdmin({ targetDir: 'resources/js/Pages', force: false })
15
+ * ```
16
+ */
17
+ export async function scaffoldAdmin(options = {}) {
18
+ const { targetDir = 'resources/js/Pages', force = false } = options;
19
+ const adminDir = join(targetDir, 'Admin');
20
+ mkdirSync(adminDir, { recursive: true });
21
+ const written = [];
22
+ const skipped = [];
23
+ for (const name of SCAFFOLD_FILES) {
24
+ const filePath = join(adminDir, name);
25
+ if (existsSync(filePath) && !force) {
26
+ skipped.push(name);
27
+ continue;
28
+ }
29
+ const content = readFileSync(join(TEMPLATES_DIR, name), 'utf-8');
30
+ writeFileSync(filePath, content, 'utf-8');
31
+ written.push(name);
32
+ }
33
+ return { written, skipped };
34
+ }
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scaffold/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAGxC,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAA;AAEtF,MAAM,cAAc,GAAG,CAAC,eAAe,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,CAAU,CAAA;AAEtG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,UAA2B,EAAE;IAC/D,MAAM,EAAE,SAAS,GAAG,oBAAoB,EAAE,KAAK,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IAEzC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAExC,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QAErC,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACnC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAClB,SAAQ;QACV,CAAC;QAED,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;QAChE,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;QACzC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;AAC7B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Template source for resources/js/Pages/Admin/Dashboard.tsx.
3
+ * Shows all admin-managed models with record counts and navigation links.
4
+ */
5
+ export declare const DASHBOARD_TEMPLATE = "import { Link } from '@inertiajs/react'\n\ninterface ModelCard {\n name: string\n urlSlug: string\n recordCount: number\n}\n\ninterface Props {\n models: ModelCard[]\n prefix: string\n}\n\nexport default function AdminDashboard({ models, prefix }: Props) {\n return (\n <div className=\"min-h-screen bg-gray-50 p-8\">\n <div className=\"max-w-5xl mx-auto\">\n <h1 className=\"text-3xl font-bold text-gray-900 mb-8\">Admin Dashboard</h1>\n\n {models.length === 0 && (\n <p className=\"text-gray-500\">No models are visible. Check your mountAdmin configuration.</p>\n )}\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {models.map((model) => (\n <Link\n key={model.urlSlug}\n href={`${prefix}/${model.urlSlug}`}\n className=\"block bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-500 transition-colors\"\n >\n <h2 className=\"text-lg font-semibold text-gray-800\">{model.name}</h2>\n <p className=\"text-3xl font-bold text-blue-600 mt-2\">{model.recordCount}</p>\n <p className=\"text-sm text-gray-500 mt-1\">records</p>\n </Link>\n ))}\n </div>\n </div>\n </div>\n )\n}\n";
6
+ /**
7
+ * Template source for resources/js/Pages/Admin/ModelIndex.tsx.
8
+ * Renders a paginated table for any model, driven entirely by the model metadata
9
+ * passed as Inertia props — no per-model customisation needed.
10
+ */
11
+ export declare const MODEL_INDEX_TEMPLATE = "import { Link, router } from '@inertiajs/react'\n\ninterface FieldMeta {\n name: string\n isId: boolean\n}\n\ninterface ModelMeta {\n name: string\n urlSlug: string\n displayName: string\n idField: string\n listFields: FieldMeta[]\n}\n\ninterface PaginationMeta {\n page: number\n perPage: number\n total: number\n totalPages: number\n}\n\ninterface Props {\n model: ModelMeta\n records: Record<string, unknown>[]\n pagination: PaginationMeta\n models: Array<{ name: string; urlSlug: string }>\n prefix: string\n}\n\nfunction displayValue(value: unknown): string {\n if (value === null || value === undefined) return '\u2014'\n if (value instanceof Date) return value.toLocaleDateString()\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\nexport default function AdminModelIndex({ model, records, pagination, models, prefix }: Props) {\n function handleDelete(id: unknown) {\n if (window.confirm(`Delete this ${model.name}?`)) {\n router.delete(`${prefix}/${model.urlSlug}/${String(id)}`)\n }\n }\n\n return (\n <div className=\"min-h-screen bg-gray-50 p-8\">\n <div className=\"max-w-6xl mx-auto\">\n <div className=\"flex items-center justify-between mb-6\">\n <div className=\"flex items-center gap-4\">\n <Link href={prefix} className=\"text-blue-600 hover:underline text-sm\">\n \u2190 Dashboard\n </Link>\n <h1 className=\"text-2xl font-bold text-gray-900\">{model.displayName}</h1>\n </div>\n <Link\n href={`${prefix}/${model.urlSlug}/new`}\n className=\"bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700\"\n >\n New {model.name}\n </Link>\n </div>\n\n {records.length === 0 ? (\n <div className=\"bg-white rounded-lg border border-gray-200 p-12 text-center\">\n <p className=\"text-gray-500\">No {model.displayName.toLowerCase()} yet.</p>\n </div>\n ) : (\n <div className=\"bg-white rounded-lg border border-gray-200 overflow-hidden\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-gray-50 border-b border-gray-200\">\n <tr>\n {model.listFields.map((f) => (\n <th key={f.name} className=\"px-4 py-3 text-left font-medium text-gray-600\">\n {f.name}\n </th>\n ))}\n <th className=\"px-4 py-3 text-right font-medium text-gray-600\">Actions</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-gray-100\">\n {records.map((record, i) => (\n <tr key={i} className=\"hover:bg-gray-50\">\n {model.listFields.map((f) => (\n <td key={f.name} className=\"px-4 py-3 text-gray-800\">\n {displayValue(record[f.name])}\n </td>\n ))}\n <td className=\"px-4 py-3 text-right space-x-2\">\n <Link\n href={`${prefix}/${model.urlSlug}/${String(record[model.idField])}`}\n className=\"text-blue-600 hover:underline\"\n >\n Edit\n </Link>\n <button\n onClick={() => handleDelete(record[model.idField])}\n className=\"text-red-600 hover:underline\"\n >\n Delete\n </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n\n {pagination.totalPages > 1 && (\n <div className=\"flex items-center justify-between mt-4 text-sm text-gray-600\">\n <span>\n Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)\n </span>\n <div className=\"flex gap-2\">\n {pagination.page > 1 && (\n <Link\n href={`${prefix}/${model.urlSlug}?page=${pagination.page - 1}`}\n className=\"px-3 py-1 border rounded hover:bg-gray-50\"\n >\n Previous\n </Link>\n )}\n {pagination.page < pagination.totalPages && (\n <Link\n href={`${prefix}/${model.urlSlug}?page=${pagination.page + 1}`}\n className=\"px-3 py-1 border rounded hover:bg-gray-50\"\n >\n Next\n </Link>\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n )\n}\n";
12
+ /**
13
+ * Template source for resources/js/Pages/Admin/ModelForm.tsx.
14
+ * Renders a create or edit form for any model using Inertia's useForm hook.
15
+ * The mode (create vs edit) is determined by whether 'record' is null.
16
+ * FK scalar fields are rendered as <select> dropdowns populated from relatedOptions.
17
+ */
18
+ export declare const MODEL_FORM_TEMPLATE = "import { useForm } from '@inertiajs/react'\nimport { Link } from '@inertiajs/react'\n\ntype FieldKind = 'scalar' | 'relation' | 'enum'\n\ninterface FieldMeta {\n name: string\n prismaType: string\n kind: FieldKind\n isRequired: boolean\n isForeignKey: boolean\n relatedModelName?: string\n enumValues?: string[]\n}\n\ninterface ModelMeta {\n name: string\n urlSlug: string\n displayName: string\n idField: string\n formFields: FieldMeta[]\n}\n\ninterface Props {\n model: ModelMeta\n record: Record<string, unknown> | null\n models: Array<{ name: string; urlSlug: string }>\n errors: Record<string, string>\n prefix: string\n relatedOptions: Record<string, Array<{ id: string; label: string }>>\n}\n\nfunction buildInitialData(\n formFields: FieldMeta[],\n record: Record<string, unknown> | null,\n): Record<string, string> {\n return Object.fromEntries(\n formFields.map((f) => {\n const value = record?.[f.name]\n if (value instanceof Date) return [f.name, value.toISOString().slice(0, 16)]\n return [f.name, value !== null && value !== undefined ? String(value) : '']\n }),\n )\n}\n\nexport default function AdminModelForm({ model, record, models, errors, prefix, relatedOptions }: Props) {\n const isEdit = record !== null\n const { data, setData, post, patch, processing } = useForm(\n buildInitialData(model.formFields, record),\n )\n\n function handleSubmit(e: React.FormEvent) {\n e.preventDefault()\n if (isEdit) {\n patch(`${prefix}/${model.urlSlug}/${String(record![model.idField])}`)\n } else {\n post(`${prefix}/${model.urlSlug}`)\n }\n }\n\n function renderInput(field: FieldMeta) {\n const value = data[field.name] ?? ''\n const error = errors[field.name]\n const baseClass = 'w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'\n const errorClass = error ? ' border-red-500' : ' border-gray-300'\n\n if (field.isForeignKey) {\n const options = relatedOptions[field.name] ?? []\n return (\n <select\n value={value}\n onChange={(e) => setData(field.name, e.target.value)}\n className={baseClass + errorClass}\n >\n {!field.isRequired && <option value=\"\">\u2014 select \u2014</option>}\n {options.map((opt) => (\n <option key={opt.id} value={opt.id}>{opt.label}</option>\n ))}\n </select>\n )\n }\n\n if (field.kind === 'enum' && field.enumValues) {\n return (\n <select\n value={value}\n onChange={(e) => setData(field.name, e.target.value)}\n className={baseClass + errorClass}\n >\n {!field.isRequired && <option value=\"\">\u2014 select \u2014</option>}\n {field.enumValues.map((v) => (\n <option key={v} value={v}>{v}</option>\n ))}\n </select>\n )\n }\n\n if (field.prismaType === 'Boolean') {\n return (\n <input\n type=\"checkbox\"\n checked={value === 'true'}\n onChange={(e) => setData(field.name, String(e.target.checked))}\n className=\"h-4 w-4 text-blue-600 border-gray-300 rounded\"\n />\n )\n }\n\n const inputType =\n field.prismaType === 'Int' || field.prismaType === 'Float'\n ? 'number'\n : field.prismaType === 'DateTime'\n ? 'datetime-local'\n : 'text'\n\n return (\n <input\n type={inputType}\n value={value}\n onChange={(e) => setData(field.name, e.target.value)}\n required={field.isRequired}\n className={baseClass + errorClass}\n />\n )\n }\n\n return (\n <div className=\"min-h-screen bg-gray-50 p-8\">\n <div className=\"max-w-2xl mx-auto\">\n <div className=\"flex items-center gap-4 mb-6\">\n <Link\n href={`${prefix}/${model.urlSlug}`}\n className=\"text-blue-600 hover:underline text-sm\"\n >\n \u2190 {model.displayName}\n </Link>\n <h1 className=\"text-2xl font-bold text-gray-900\">\n {isEdit ? `Edit ${model.name}` : `New ${model.name}`}\n </h1>\n </div>\n\n <form onSubmit={handleSubmit} className=\"bg-white rounded-lg border border-gray-200 p-6 space-y-5\">\n {model.formFields.map((field) => (\n <div key={field.name}>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n {field.name}\n {field.isRequired && <span className=\"text-red-500 ml-1\">*</span>}\n </label>\n {renderInput(field)}\n {errors[field.name] && (\n <p className=\"mt-1 text-xs text-red-600\">{errors[field.name]}</p>\n )}\n </div>\n ))}\n\n <div className=\"flex items-center gap-3 pt-2\">\n <button\n type=\"submit\"\n disabled={processing}\n className=\"bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50\"\n >\n {processing ? 'Saving\u2026' : isEdit ? 'Update' : 'Create'}\n </button>\n <Link\n href={`${prefix}/${model.urlSlug}`}\n className=\"text-gray-600 hover:underline text-sm\"\n >\n Cancel\n </Link>\n </div>\n </form>\n </div>\n </div>\n )\n}\n";
19
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/scaffold/templates.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,uyCAwC9B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,qwJAyIhC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,o3KA+K/B,CAAA"}
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Template source for resources/js/Pages/Admin/Dashboard.tsx.
3
+ * Shows all admin-managed models with record counts and navigation links.
4
+ */
5
+ export const DASHBOARD_TEMPLATE = `import { Link } from '@inertiajs/react'
6
+
7
+ interface ModelCard {
8
+ name: string
9
+ urlSlug: string
10
+ recordCount: number
11
+ }
12
+
13
+ interface Props {
14
+ models: ModelCard[]
15
+ prefix: string
16
+ }
17
+
18
+ export default function AdminDashboard({ models, prefix }: Props) {
19
+ return (
20
+ <div className="min-h-screen bg-gray-50 p-8">
21
+ <div className="max-w-5xl mx-auto">
22
+ <h1 className="text-3xl font-bold text-gray-900 mb-8">Admin Dashboard</h1>
23
+
24
+ {models.length === 0 && (
25
+ <p className="text-gray-500">No models are visible. Check your mountAdmin configuration.</p>
26
+ )}
27
+
28
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
29
+ {models.map((model) => (
30
+ <Link
31
+ key={model.urlSlug}
32
+ href={\`\${prefix}/\${model.urlSlug}\`}
33
+ className="block bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-500 transition-colors"
34
+ >
35
+ <h2 className="text-lg font-semibold text-gray-800">{model.name}</h2>
36
+ <p className="text-3xl font-bold text-blue-600 mt-2">{model.recordCount}</p>
37
+ <p className="text-sm text-gray-500 mt-1">records</p>
38
+ </Link>
39
+ ))}
40
+ </div>
41
+ </div>
42
+ </div>
43
+ )
44
+ }
45
+ `;
46
+ /**
47
+ * Template source for resources/js/Pages/Admin/ModelIndex.tsx.
48
+ * Renders a paginated table for any model, driven entirely by the model metadata
49
+ * passed as Inertia props — no per-model customisation needed.
50
+ */
51
+ export const MODEL_INDEX_TEMPLATE = `import { Link, router } from '@inertiajs/react'
52
+
53
+ interface FieldMeta {
54
+ name: string
55
+ isId: boolean
56
+ }
57
+
58
+ interface ModelMeta {
59
+ name: string
60
+ urlSlug: string
61
+ displayName: string
62
+ idField: string
63
+ listFields: FieldMeta[]
64
+ }
65
+
66
+ interface PaginationMeta {
67
+ page: number
68
+ perPage: number
69
+ total: number
70
+ totalPages: number
71
+ }
72
+
73
+ interface Props {
74
+ model: ModelMeta
75
+ records: Record<string, unknown>[]
76
+ pagination: PaginationMeta
77
+ models: Array<{ name: string; urlSlug: string }>
78
+ prefix: string
79
+ }
80
+
81
+ function displayValue(value: unknown): string {
82
+ if (value === null || value === undefined) return '—'
83
+ if (value instanceof Date) return value.toLocaleDateString()
84
+ if (typeof value === 'object') return JSON.stringify(value)
85
+ return String(value)
86
+ }
87
+
88
+ export default function AdminModelIndex({ model, records, pagination, models, prefix }: Props) {
89
+ function handleDelete(id: unknown) {
90
+ if (window.confirm(\`Delete this \${model.name}?\`)) {
91
+ router.delete(\`\${prefix}/\${model.urlSlug}/\${String(id)}\`)
92
+ }
93
+ }
94
+
95
+ return (
96
+ <div className="min-h-screen bg-gray-50 p-8">
97
+ <div className="max-w-6xl mx-auto">
98
+ <div className="flex items-center justify-between mb-6">
99
+ <div className="flex items-center gap-4">
100
+ <Link href={prefix} className="text-blue-600 hover:underline text-sm">
101
+ ← Dashboard
102
+ </Link>
103
+ <h1 className="text-2xl font-bold text-gray-900">{model.displayName}</h1>
104
+ </div>
105
+ <Link
106
+ href={\`\${prefix}/\${model.urlSlug}/new\`}
107
+ className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
108
+ >
109
+ New {model.name}
110
+ </Link>
111
+ </div>
112
+
113
+ {records.length === 0 ? (
114
+ <div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
115
+ <p className="text-gray-500">No {model.displayName.toLowerCase()} yet.</p>
116
+ </div>
117
+ ) : (
118
+ <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
119
+ <table className="w-full text-sm">
120
+ <thead className="bg-gray-50 border-b border-gray-200">
121
+ <tr>
122
+ {model.listFields.map((f) => (
123
+ <th key={f.name} className="px-4 py-3 text-left font-medium text-gray-600">
124
+ {f.name}
125
+ </th>
126
+ ))}
127
+ <th className="px-4 py-3 text-right font-medium text-gray-600">Actions</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody className="divide-y divide-gray-100">
131
+ {records.map((record, i) => (
132
+ <tr key={i} className="hover:bg-gray-50">
133
+ {model.listFields.map((f) => (
134
+ <td key={f.name} className="px-4 py-3 text-gray-800">
135
+ {displayValue(record[f.name])}
136
+ </td>
137
+ ))}
138
+ <td className="px-4 py-3 text-right space-x-2">
139
+ <Link
140
+ href={\`\${prefix}/\${model.urlSlug}/\${String(record[model.idField])}\`}
141
+ className="text-blue-600 hover:underline"
142
+ >
143
+ Edit
144
+ </Link>
145
+ <button
146
+ onClick={() => handleDelete(record[model.idField])}
147
+ className="text-red-600 hover:underline"
148
+ >
149
+ Delete
150
+ </button>
151
+ </td>
152
+ </tr>
153
+ ))}
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ )}
158
+
159
+ {pagination.totalPages > 1 && (
160
+ <div className="flex items-center justify-between mt-4 text-sm text-gray-600">
161
+ <span>
162
+ Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
163
+ </span>
164
+ <div className="flex gap-2">
165
+ {pagination.page > 1 && (
166
+ <Link
167
+ href={\`\${prefix}/\${model.urlSlug}?page=\${pagination.page - 1}\`}
168
+ className="px-3 py-1 border rounded hover:bg-gray-50"
169
+ >
170
+ Previous
171
+ </Link>
172
+ )}
173
+ {pagination.page < pagination.totalPages && (
174
+ <Link
175
+ href={\`\${prefix}/\${model.urlSlug}?page=\${pagination.page + 1}\`}
176
+ className="px-3 py-1 border rounded hover:bg-gray-50"
177
+ >
178
+ Next
179
+ </Link>
180
+ )}
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ )
187
+ }
188
+ `;
189
+ /**
190
+ * Template source for resources/js/Pages/Admin/ModelForm.tsx.
191
+ * Renders a create or edit form for any model using Inertia's useForm hook.
192
+ * The mode (create vs edit) is determined by whether 'record' is null.
193
+ * FK scalar fields are rendered as <select> dropdowns populated from relatedOptions.
194
+ */
195
+ export const MODEL_FORM_TEMPLATE = `import { useForm } from '@inertiajs/react'
196
+ import { Link } from '@inertiajs/react'
197
+
198
+ type FieldKind = 'scalar' | 'relation' | 'enum'
199
+
200
+ interface FieldMeta {
201
+ name: string
202
+ prismaType: string
203
+ kind: FieldKind
204
+ isRequired: boolean
205
+ isForeignKey: boolean
206
+ relatedModelName?: string
207
+ enumValues?: string[]
208
+ }
209
+
210
+ interface ModelMeta {
211
+ name: string
212
+ urlSlug: string
213
+ displayName: string
214
+ idField: string
215
+ formFields: FieldMeta[]
216
+ }
217
+
218
+ interface Props {
219
+ model: ModelMeta
220
+ record: Record<string, unknown> | null
221
+ models: Array<{ name: string; urlSlug: string }>
222
+ errors: Record<string, string>
223
+ prefix: string
224
+ relatedOptions: Record<string, Array<{ id: string; label: string }>>
225
+ }
226
+
227
+ function buildInitialData(
228
+ formFields: FieldMeta[],
229
+ record: Record<string, unknown> | null,
230
+ ): Record<string, string> {
231
+ return Object.fromEntries(
232
+ formFields.map((f) => {
233
+ const value = record?.[f.name]
234
+ if (value instanceof Date) return [f.name, value.toISOString().slice(0, 16)]
235
+ return [f.name, value !== null && value !== undefined ? String(value) : '']
236
+ }),
237
+ )
238
+ }
239
+
240
+ export default function AdminModelForm({ model, record, models, errors, prefix, relatedOptions }: Props) {
241
+ const isEdit = record !== null
242
+ const { data, setData, post, patch, processing } = useForm(
243
+ buildInitialData(model.formFields, record),
244
+ )
245
+
246
+ function handleSubmit(e: React.FormEvent) {
247
+ e.preventDefault()
248
+ if (isEdit) {
249
+ patch(\`\${prefix}/\${model.urlSlug}/\${String(record![model.idField])}\`)
250
+ } else {
251
+ post(\`\${prefix}/\${model.urlSlug}\`)
252
+ }
253
+ }
254
+
255
+ function renderInput(field: FieldMeta) {
256
+ const value = data[field.name] ?? ''
257
+ const error = errors[field.name]
258
+ const baseClass = 'w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
259
+ const errorClass = error ? ' border-red-500' : ' border-gray-300'
260
+
261
+ if (field.isForeignKey) {
262
+ const options = relatedOptions[field.name] ?? []
263
+ return (
264
+ <select
265
+ value={value}
266
+ onChange={(e) => setData(field.name, e.target.value)}
267
+ className={baseClass + errorClass}
268
+ >
269
+ {!field.isRequired && <option value="">— select —</option>}
270
+ {options.map((opt) => (
271
+ <option key={opt.id} value={opt.id}>{opt.label}</option>
272
+ ))}
273
+ </select>
274
+ )
275
+ }
276
+
277
+ if (field.kind === 'enum' && field.enumValues) {
278
+ return (
279
+ <select
280
+ value={value}
281
+ onChange={(e) => setData(field.name, e.target.value)}
282
+ className={baseClass + errorClass}
283
+ >
284
+ {!field.isRequired && <option value="">— select —</option>}
285
+ {field.enumValues.map((v) => (
286
+ <option key={v} value={v}>{v}</option>
287
+ ))}
288
+ </select>
289
+ )
290
+ }
291
+
292
+ if (field.prismaType === 'Boolean') {
293
+ return (
294
+ <input
295
+ type="checkbox"
296
+ checked={value === 'true'}
297
+ onChange={(e) => setData(field.name, String(e.target.checked))}
298
+ className="h-4 w-4 text-blue-600 border-gray-300 rounded"
299
+ />
300
+ )
301
+ }
302
+
303
+ const inputType =
304
+ field.prismaType === 'Int' || field.prismaType === 'Float'
305
+ ? 'number'
306
+ : field.prismaType === 'DateTime'
307
+ ? 'datetime-local'
308
+ : 'text'
309
+
310
+ return (
311
+ <input
312
+ type={inputType}
313
+ value={value}
314
+ onChange={(e) => setData(field.name, e.target.value)}
315
+ required={field.isRequired}
316
+ className={baseClass + errorClass}
317
+ />
318
+ )
319
+ }
320
+
321
+ return (
322
+ <div className="min-h-screen bg-gray-50 p-8">
323
+ <div className="max-w-2xl mx-auto">
324
+ <div className="flex items-center gap-4 mb-6">
325
+ <Link
326
+ href={\`\${prefix}/\${model.urlSlug}\`}
327
+ className="text-blue-600 hover:underline text-sm"
328
+ >
329
+ ← {model.displayName}
330
+ </Link>
331
+ <h1 className="text-2xl font-bold text-gray-900">
332
+ {isEdit ? \`Edit \${model.name}\` : \`New \${model.name}\`}
333
+ </h1>
334
+ </div>
335
+
336
+ <form onSubmit={handleSubmit} className="bg-white rounded-lg border border-gray-200 p-6 space-y-5">
337
+ {model.formFields.map((field) => (
338
+ <div key={field.name}>
339
+ <label className="block text-sm font-medium text-gray-700 mb-1">
340
+ {field.name}
341
+ {field.isRequired && <span className="text-red-500 ml-1">*</span>}
342
+ </label>
343
+ {renderInput(field)}
344
+ {errors[field.name] && (
345
+ <p className="mt-1 text-xs text-red-600">{errors[field.name]}</p>
346
+ )}
347
+ </div>
348
+ ))}
349
+
350
+ <div className="flex items-center gap-3 pt-2">
351
+ <button
352
+ type="submit"
353
+ disabled={processing}
354
+ className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
355
+ >
356
+ {processing ? 'Saving…' : isEdit ? 'Update' : 'Create'}
357
+ </button>
358
+ <Link
359
+ href={\`\${prefix}/\${model.urlSlug}\`}
360
+ className="text-gray-600 hover:underline text-sm"
361
+ >
362
+ Cancel
363
+ </Link>
364
+ </div>
365
+ </form>
366
+ </div>
367
+ </div>
368
+ )
369
+ }
370
+ `;
371
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/scaffold/templates.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCjC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyInC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+KlC,CAAA"}
@@ -0,0 +1,40 @@
1
+ import { Link } from '@inertiajs/react'
2
+
3
+ interface ModelCard {
4
+ name: string
5
+ urlSlug: string
6
+ recordCount: number
7
+ }
8
+
9
+ interface Props {
10
+ models: ModelCard[]
11
+ prefix: string
12
+ }
13
+
14
+ export default function AdminDashboard({ models, prefix }: Props) {
15
+ return (
16
+ <div className="min-h-screen bg-gray-50 p-8">
17
+ <div className="max-w-5xl mx-auto">
18
+ <h1 className="text-3xl font-bold text-gray-900 mb-8">Admin Dashboard</h1>
19
+
20
+ {models.length === 0 && (
21
+ <p className="text-gray-500">No models are visible. Check your mountAdmin configuration.</p>
22
+ )}
23
+
24
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
25
+ {models.map((model) => (
26
+ <Link
27
+ key={model.urlSlug}
28
+ href={`${prefix}/${model.urlSlug}`}
29
+ className="block bg-white rounded-lg border border-gray-200 p-6 hover:border-blue-500 transition-colors"
30
+ >
31
+ <h2 className="text-lg font-semibold text-gray-800">{model.name}</h2>
32
+ <p className="text-3xl font-bold text-blue-600 mt-2">{model.recordCount}</p>
33
+ <p className="text-sm text-gray-500 mt-1">records</p>
34
+ </Link>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ </div>
39
+ )
40
+ }