@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
@@ -0,0 +1,142 @@
1
+ import { Link, router } from '@inertiajs/react'
2
+
3
+ interface FieldMeta {
4
+ name: string
5
+ isId: boolean
6
+ prismaType: string
7
+ }
8
+
9
+ interface ModelMeta {
10
+ name: string
11
+ urlSlug: string
12
+ displayName: string
13
+ idField: string
14
+ listFields: FieldMeta[]
15
+ }
16
+
17
+ interface PaginationMeta {
18
+ page: number
19
+ perPage: number
20
+ total: number
21
+ totalPages: number
22
+ }
23
+
24
+ interface Props {
25
+ model: ModelMeta
26
+ records: Record<string, unknown>[]
27
+ pagination: PaginationMeta
28
+ models: Array<{ name: string; urlSlug: string }>
29
+ prefix: string
30
+ }
31
+
32
+ function displayValue(value: unknown, prismaType: string): string {
33
+ if (value === null || value === undefined) return '—'
34
+ if (prismaType === 'DateTime') {
35
+ const date = value instanceof Date ? value : new Date(String(value))
36
+ return date.toLocaleString()
37
+ }
38
+ if (value instanceof Date) return value.toLocaleString()
39
+ if (typeof value === 'object') return JSON.stringify(value)
40
+ return String(value)
41
+ }
42
+
43
+ export default function AdminModelIndex({ model, records, pagination, prefix }: Props) {
44
+ function handleDelete(id: unknown) {
45
+ if (window.confirm(`Delete this ${model.name}?`)) {
46
+ router.delete(`${prefix}/${model.urlSlug}/${String(id)}`)
47
+ }
48
+ }
49
+
50
+ return (
51
+ <div className="min-h-screen bg-gray-50 p-8">
52
+ <div className="max-w-6xl mx-auto">
53
+ <div className="flex items-center justify-between mb-6">
54
+ <div className="flex items-center gap-4">
55
+ <Link href={prefix} className="text-blue-600 hover:underline text-sm">
56
+ ← Dashboard
57
+ </Link>
58
+ <h1 className="text-2xl font-bold text-gray-900">{model.displayName}</h1>
59
+ </div>
60
+ <Link
61
+ href={`${prefix}/${model.urlSlug}/new`}
62
+ className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
63
+ >
64
+ New {model.name}
65
+ </Link>
66
+ </div>
67
+
68
+ {records.length === 0 ? (
69
+ <div className="bg-white rounded-lg border border-gray-200 p-12 text-center">
70
+ <p className="text-gray-500">No {model.displayName.toLowerCase()} yet.</p>
71
+ </div>
72
+ ) : (
73
+ <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
74
+ <table className="w-full text-sm">
75
+ <thead className="bg-gray-50 border-b border-gray-200">
76
+ <tr>
77
+ {model.listFields.map((f) => (
78
+ <th key={f.name} className="px-4 py-3 text-left font-medium text-gray-600">
79
+ {f.name}
80
+ </th>
81
+ ))}
82
+ <th className="px-4 py-3 text-right font-medium text-gray-600">Actions</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody className="divide-y divide-gray-100">
86
+ {records.map((record) => (
87
+ <tr key={String(record[model.idField])} className="hover:bg-gray-50">
88
+ {model.listFields.map((f) => (
89
+ <td key={f.name} className="px-4 py-3 text-gray-800">
90
+ {displayValue(record[f.name], f.prismaType)}
91
+ </td>
92
+ ))}
93
+ <td className="px-4 py-3 text-right space-x-2">
94
+ <Link
95
+ href={`${prefix}/${model.urlSlug}/${String(record[model.idField])}`}
96
+ className="text-blue-600 hover:underline"
97
+ >
98
+ Edit
99
+ </Link>
100
+ <button
101
+ onClick={() => handleDelete(record[model.idField])}
102
+ className="text-red-600 hover:underline"
103
+ >
104
+ Delete
105
+ </button>
106
+ </td>
107
+ </tr>
108
+ ))}
109
+ </tbody>
110
+ </table>
111
+ </div>
112
+ )}
113
+
114
+ {pagination.totalPages > 1 && (
115
+ <div className="flex items-center justify-between mt-4 text-sm text-gray-600">
116
+ <span>
117
+ Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
118
+ </span>
119
+ <div className="flex gap-2">
120
+ {pagination.page > 1 && (
121
+ <Link
122
+ href={`${prefix}/${model.urlSlug}?page=${pagination.page - 1}`}
123
+ className="px-3 py-1 border rounded hover:bg-gray-50"
124
+ >
125
+ Previous
126
+ </Link>
127
+ )}
128
+ {pagination.page < pagination.totalPages && (
129
+ <Link
130
+ href={`${prefix}/${model.urlSlug}?page=${pagination.page + 1}`}
131
+ className="px-3 py-1 border rounded hover:bg-gray-50"
132
+ >
133
+ Next
134
+ </Link>
135
+ )}
136
+ </div>
137
+ </div>
138
+ )}
139
+ </div>
140
+ </div>
141
+ )
142
+ }
@@ -0,0 +1,189 @@
1
+ import { useForm, Link } from '@inertiajs/react'
2
+
3
+ type FieldKind = 'scalar' | 'relation' | 'enum'
4
+
5
+ interface FieldMeta {
6
+ name: string
7
+ prismaType: string
8
+ kind: FieldKind
9
+ isRequired: boolean
10
+ enumValues?: string[]
11
+ }
12
+
13
+ interface ModelMeta {
14
+ name: string
15
+ urlSlug: string
16
+ displayName: string
17
+ formFields: FieldMeta[]
18
+ }
19
+
20
+ interface Props {
21
+ model: ModelMeta
22
+ models: Array<{ name: string; urlSlug: string }>
23
+ errors: Record<string, string>
24
+ prefix: string
25
+ }
26
+
27
+ /**
28
+ * Fields rendered by dedicated hardcoded inputs — excluded from the generic
29
+ * metadata loop. `password` is also hardcoded but is not a Prisma field and
30
+ * therefore never appears in model.formFields.
31
+ */
32
+ const CORE_FIELD_NAMES = new Set(['name', 'email'])
33
+
34
+ export default function AdminUserCreate({ model, errors, prefix }: Props) {
35
+ // All formFields except the two that are rendered by dedicated hardcoded
36
+ // inputs. Includes `role` (if present) and any custom fields.
37
+ const extraFields = model.formFields.filter((f) => !CORE_FIELD_NAMES.has(f.name))
38
+
39
+ const extraInitialValues = Object.fromEntries(
40
+ extraFields.map((f) => [
41
+ f.name,
42
+ f.kind === 'enum' && f.enumValues != null && f.enumValues.length > 0
43
+ ? (f.enumValues[0] ?? '')
44
+ : '',
45
+ ]),
46
+ )
47
+
48
+ const { data, setData, post, processing } = useForm<Record<string, string>>({
49
+ name: '',
50
+ email: '',
51
+ password: '',
52
+ ...extraInitialValues,
53
+ })
54
+
55
+ function handleSubmit(e: React.FormEvent) {
56
+ e.preventDefault()
57
+ post(`${prefix}/${model.urlSlug}`)
58
+ }
59
+
60
+ const inputClass =
61
+ 'w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500'
62
+
63
+ function borderClass(field: string): string {
64
+ return errors[field] ? ' border-red-500' : ' border-gray-300'
65
+ }
66
+
67
+ return (
68
+ <div className="min-h-screen bg-gray-50 p-8">
69
+ <div className="max-w-2xl mx-auto">
70
+ <div className="flex items-center gap-4 mb-6">
71
+ <Link
72
+ href={`${prefix}/${model.urlSlug}`}
73
+ className="text-blue-600 hover:underline text-sm"
74
+ >
75
+ ← {model.displayName}
76
+ </Link>
77
+ <h1 className="text-2xl font-bold text-gray-900">New {model.name}</h1>
78
+ </div>
79
+
80
+ <form
81
+ onSubmit={handleSubmit}
82
+ className="bg-white rounded-lg border border-gray-200 p-6 space-y-5"
83
+ >
84
+ {/* name — hardcoded: always required by the Better Auth createUser API */}
85
+ <div>
86
+ <label className="block text-sm font-medium text-gray-700 mb-1">
87
+ name <span className="text-red-500">*</span>
88
+ </label>
89
+ <input
90
+ type="text"
91
+ value={data['name']}
92
+ onChange={(e) => setData('name', e.target.value)}
93
+ required
94
+ className={inputClass + borderClass('name')}
95
+ />
96
+ {errors['name'] && (
97
+ <p className="mt-1 text-xs text-red-600">{errors['name']}</p>
98
+ )}
99
+ </div>
100
+
101
+ {/* email — hardcoded: always required by the Better Auth createUser API */}
102
+ <div>
103
+ <label className="block text-sm font-medium text-gray-700 mb-1">
104
+ email <span className="text-red-500">*</span>
105
+ </label>
106
+ <input
107
+ type="email"
108
+ value={data['email']}
109
+ onChange={(e) => setData('email', e.target.value)}
110
+ required
111
+ className={inputClass + borderClass('email')}
112
+ />
113
+ {errors['email'] && (
114
+ <p className="mt-1 text-xs text-red-600">{errors['email']}</p>
115
+ )}
116
+ </div>
117
+
118
+ {/* password — hardcoded: always required by the Better Auth createUser API.
119
+ Not a Prisma field so it never appears in model.formFields. */}
120
+ <div>
121
+ <label className="block text-sm font-medium text-gray-700 mb-1">
122
+ password <span className="text-red-500">*</span>
123
+ </label>
124
+ <input
125
+ type="password"
126
+ value={data['password']}
127
+ onChange={(e) => setData('password', e.target.value)}
128
+ required
129
+ className={inputClass + borderClass('password')}
130
+ />
131
+ {errors['password'] && (
132
+ <p className="mt-1 text-xs text-red-600">{errors['password']}</p>
133
+ )}
134
+ </div>
135
+
136
+ {/* role and any custom fields — driven from model.formFields metadata */}
137
+ {extraFields.map((field) => (
138
+ <div key={field.name}>
139
+ <label className="block text-sm font-medium text-gray-700 mb-1">
140
+ {field.name}
141
+ {field.isRequired && <span className="text-red-500"> *</span>}
142
+ </label>
143
+ {field.kind === 'enum' && field.enumValues != null ? (
144
+ <select
145
+ value={data[field.name] ?? ''}
146
+ onChange={(e) => setData(field.name, e.target.value)}
147
+ className={inputClass + borderClass(field.name)}
148
+ >
149
+ {field.enumValues.map((v) => (
150
+ <option key={v} value={v}>
151
+ {v}
152
+ </option>
153
+ ))}
154
+ </select>
155
+ ) : (
156
+ <input
157
+ type="text"
158
+ value={data[field.name] ?? ''}
159
+ onChange={(e) => setData(field.name, e.target.value)}
160
+ required={field.isRequired}
161
+ className={inputClass + borderClass(field.name)}
162
+ />
163
+ )}
164
+ {errors[field.name] && (
165
+ <p className="mt-1 text-xs text-red-600">{errors[field.name]}</p>
166
+ )}
167
+ </div>
168
+ ))}
169
+
170
+ <div className="flex items-center gap-3 pt-2">
171
+ <button
172
+ type="submit"
173
+ disabled={processing}
174
+ className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50"
175
+ >
176
+ {processing ? 'Creating…' : 'Create'}
177
+ </button>
178
+ <Link
179
+ href={`${prefix}/${model.urlSlug}`}
180
+ className="text-gray-600 hover:underline text-sm"
181
+ >
182
+ Cancel
183
+ </Link>
184
+ </div>
185
+ </form>
186
+ </div>
187
+ </div>
188
+ )
189
+ }