betterstart-cli 0.0.117 → 0.0.119

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 (87) hide show
  1. package/dist/assets/adapters/next/templates/init/admin-globals.css +154 -38
  2. package/dist/assets/adapters/next/templates/init/components/shared/data-table/data-grid-row.tsx +33 -1
  3. package/dist/assets/adapters/next/templates/init/components/shared/media/media-gallery-dialog.tsx +3 -3
  4. package/dist/assets/adapters/next/templates/init/components/shared/media/media-preview.tsx +3 -1
  5. package/dist/assets/adapters/next/templates/init/hooks/use-field-error-visible.ts +25 -0
  6. package/dist/assets/adapters/next/templates/init/hooks/use-media.ts +1 -1
  7. package/dist/assets/adapters/next/templates/init/hooks/use-upload.ts +11 -5
  8. package/dist/assets/adapters/next/templates/init/lib/actions/audit/cleanup-audit-logs.ts +1 -0
  9. package/dist/assets/adapters/next/templates/init/lib/actions/audit/query-audit-logs.ts +2 -2
  10. package/dist/assets/adapters/next/templates/init/lib/actions/audit/record-audit-event.ts +1 -2
  11. package/dist/assets/adapters/next/templates/init/lib/actions/auth/client.ts +4 -1
  12. package/dist/assets/adapters/next/templates/init/lib/actions/entity-versions/get-entity-versions.ts +5 -1
  13. package/dist/assets/adapters/next/templates/init/lib/actions/forms/get-all-form-settings.ts +1 -1
  14. package/dist/assets/adapters/next/templates/init/lib/actions/media/create-media.ts +2 -1
  15. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media-bulk.ts +1 -0
  16. package/dist/assets/adapters/next/templates/init/lib/actions/media/delete-media.ts +1 -0
  17. package/dist/assets/adapters/next/templates/init/lib/actions/media/update-media.ts +2 -4
  18. package/dist/assets/adapters/next/templates/init/lib/actions/profile/change-own-password.ts +1 -0
  19. package/dist/assets/adapters/next/templates/init/lib/actions/profile/update-email.ts +1 -0
  20. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-files.ts +1 -0
  21. package/dist/assets/adapters/next/templates/init/lib/actions/upload/upload-media-from-url.ts +16 -6
  22. package/dist/assets/adapters/next/templates/init/lib/actions/users/change-user-password.ts +1 -0
  23. package/dist/assets/adapters/next/templates/init/lib/actions/users/create-user.ts +2 -1
  24. package/dist/assets/adapters/next/templates/init/lib/actions/users/delete-user.ts +1 -0
  25. package/dist/assets/adapters/next/templates/init/lib/actions/users/types.ts +1 -1
  26. package/dist/assets/adapters/next/templates/init/lib/actions/users/update-user-role.ts +1 -0
  27. package/dist/assets/adapters/next/templates/init/lib/db/core/schema.ts +12 -4
  28. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/registry.ts +4 -4
  29. package/dist/assets/adapters/next/templates/init/lib/lifecycle-hooks/runner.ts +3 -7
  30. package/dist/assets/adapters/next/templates/init/pages/forgot-password-form.tsx +5 -8
  31. package/dist/assets/adapters/next/templates/init/pages/forgot-password-page-skeleton.tsx +2 -2
  32. package/dist/assets/adapters/next/templates/init/pages/login-form.tsx +7 -9
  33. package/dist/assets/adapters/next/templates/init/pages/login-page-skeleton.tsx +2 -2
  34. package/dist/assets/adapters/next/templates/init/pages/reset-password-form.tsx +8 -16
  35. package/dist/assets/adapters/next/templates/init/pages/reset-password-page-skeleton.tsx +2 -2
  36. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-detail-row.tsx +1 -1
  37. package/dist/assets/adapters/next/templates/init/pages/settings/audit-log/audit-log-table.tsx +1 -6
  38. package/dist/assets/adapters/next/templates/init/pages/settings/forms/forms-settings-table.tsx +2 -7
  39. package/dist/assets/adapters/next/templates/init/pages/settings/webhooks/webhooks-table.tsx +1 -6
  40. package/dist/assets/adapters/next/templates/init/pages/users/users-table.tsx +1 -1
  41. package/dist/assets/adapters/next/templates/init/public/placeholder.png +0 -0
  42. package/dist/assets/adapters/next/templates/init/types/index.ts +2 -0
  43. package/dist/assets/adapters/next/templates/init/utils/audit/normalize-comparable-value.ts +4 -2
  44. package/dist/assets/adapters/next/templates/init/utils/date/format-action-date.ts +12 -0
  45. package/dist/assets/adapters/next/templates/init/utils/dev-mode/dev-mode-json-schemas.ts +66 -0
  46. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-lifecycle-hooks.ts +11 -0
  47. package/dist/assets/adapters/next/templates/init/utils/dev-mode/parse-dev-mode-snippets.ts +9 -0
  48. package/dist/assets/adapters/next/templates/init/utils/editor/set-task-item-checked.ts +13 -0
  49. package/dist/assets/adapters/next/templates/init/utils/editor/update-nodes-attr.ts +2 -1
  50. package/dist/assets/adapters/next/templates/init/utils/email/email-theme.ts +13 -0
  51. package/dist/assets/adapters/next/templates/init/utils/email/render-form-submission-rows.ts +3 -2
  52. package/dist/assets/adapters/next/templates/init/utils/email/render-notification-email.ts +7 -6
  53. package/dist/assets/adapters/next/templates/init/utils/email/render-thank-you-email.ts +5 -4
  54. package/dist/assets/adapters/next/templates/init/utils/error/read-string-property.ts +4 -2
  55. package/dist/assets/adapters/next/templates/init/utils/error/recognize.ts +2 -4
  56. package/dist/assets/adapters/next/templates/init/utils/error/to-action-error.ts +0 -1
  57. package/dist/assets/adapters/next/templates/init/utils/form/clone-default-value.ts +4 -2
  58. package/dist/assets/adapters/next/templates/init/utils/shared/is-record.ts +3 -0
  59. package/dist/assets/adapters/next/templates/init/utils/table/default-get-id.ts +3 -2
  60. package/dist/assets/shared-assets/react-admin/custom/content-editor/editor-toolbar.tsx +1 -1
  61. package/dist/assets/shared-assets/react-admin/custom/content-editor/slash-command-menu.tsx +5 -3
  62. package/dist/assets/shared-assets/react-admin/custom/content-editor/source-mode.tsx +1 -1
  63. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/removable-image-node/removable-image-node.tsx +3 -1
  64. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-node/task-item-node/task-item-node-extension.tsx +6 -15
  65. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/heading-dropdown-menu/heading-dropdown-menu.tsx +1 -6
  66. package/dist/assets/shared-assets/react-admin/custom/content-editor/tiptap-ui/list-dropdown-menu/list-dropdown-menu.tsx +1 -6
  67. package/dist/assets/shared-assets/react-admin/custom/date-range-picker.tsx +9 -2
  68. package/dist/assets/shared-assets/react-admin/custom/gallery-field.tsx +3 -3
  69. package/dist/assets/shared-assets/react-admin/custom/media-field.tsx +2 -2
  70. package/dist/assets/shared-assets/react-admin/custom/nested-object-list-field.tsx +3 -1
  71. package/dist/assets/shared-assets/react-admin/custom/progressive-blur.tsx +3 -1
  72. package/dist/assets/shared-assets/react-admin/ui/badge.tsx +1 -2
  73. package/dist/assets/shared-assets/react-admin/ui/dropdown-menu.tsx +20 -23
  74. package/dist/assets/shared-assets/react-admin/ui/form.tsx +6 -3
  75. package/dist/assets/shared-assets/react-admin/ui/input.tsx +1 -1
  76. package/dist/assets/shared-assets/react-admin/ui/native-select.tsx +1 -1
  77. package/dist/assets/shared-assets/react-admin/ui/navigation-menu.tsx +1 -1
  78. package/dist/assets/shared-assets/react-admin/ui/select.tsx +1 -1
  79. package/dist/assets/shared-assets/react-admin/ui/textarea.tsx +1 -1
  80. package/dist/{chunk-FGEVDA53.js → chunk-BNHFSHPE.js} +12 -1
  81. package/dist/chunk-BNHFSHPE.js.map +1 -0
  82. package/dist/cli.js +909 -598
  83. package/dist/cli.js.map +1 -1
  84. package/dist/{read-namespaced-template-KUENIIDC.js → read-namespaced-template-WYIGUH7S.js} +2 -2
  85. package/package.json +1 -1
  86. package/dist/chunk-FGEVDA53.js.map +0 -1
  87. /package/dist/{read-namespaced-template-KUENIIDC.js.map → read-namespaced-template-WYIGUH7S.js.map} +0 -0
@@ -27,7 +27,9 @@
27
27
  --color-input: var(--input);
28
28
  --color-border: var(--border);
29
29
  --color-highlight: var(--highlight);
30
+ --color-placeholder: var(--placeholder);
30
31
  --color-destructive: var(--destructive);
32
+ --color-success: var(--success);
31
33
  --color-warning: var(--warning);
32
34
  --color-warning-foreground: var(--warning-foreground);
33
35
  --color-accent-foreground: var(--accent-foreground);
@@ -80,36 +82,152 @@
80
82
  );
81
83
  }
82
84
 
85
+ @theme {
86
+ --breakpoint-xs: 480px;
87
+ --spacing-entity-editor: 300px;
88
+ --spacing-table-cell: 300px;
89
+ --spacing-form-settings-name: 200px;
90
+ --spacing-form-settings-recipients: 240px;
91
+ --spacing-auth-login-panel: 540px;
92
+ --spacing-auth-reset-panel: 520px;
93
+ --spacing-auth-forgot-panel: 460px;
94
+ --spacing-media-dialog: 700px;
95
+ --spacing-media-tile: 180px;
96
+ --spacing-media-thumb: 100px;
97
+ --spacing-gallery-item: 184px;
98
+ --spacing-date-range-trigger: 206px;
99
+ --spacing-date-range-popover: 282px;
100
+ --spacing-source-editor: 240px;
101
+ --grid-template-columns-entity-form: 1fr 360px;
102
+ --grid-template-columns-audit-detail: 120px 1fr;
103
+ }
104
+
83
105
  :root {
84
- --card: oklch(1 0 0);
85
- --card-foreground: oklch(0.145 0 0);
86
- --popover: oklch(1 0 0);
87
- --popover-foreground: oklch(0.145 0 0);
88
- --primary: oklch(0.15 0 0);
89
- --primary-foreground: oklch(0.985 0 0);
90
- --secondary: oklch(0.95 0 0);
91
- --secondary-foreground: oklch(0.15 0 0);
92
- --muted: oklch(0.97 0 0);
93
- --muted-foreground: oklch(0.556 0 0);
94
- --accent: oklch(0.97 0 0);
95
- --accent-foreground: oklch(0.15 0 0);
96
- --destructive: oklch(0.577 0.245 27.325);
97
- --warning: oklch(0.625 0.165 57.4);
98
- --warning-foreground: oklch(0.625 0.165 57.4);
99
- --border: oklch(0.93 0 0);
100
- --highlight: oklch(0.9 0.004 286.32);
101
- --input: oklch(0.93 0 0);
102
- --ring: oklch(0.708 0 0);
103
- --chart-1: oklch(0.845 0.143 164.978);
104
- --chart-2: oklch(0.696 0.17 162.48);
105
- --chart-3: oklch(0.596 0.145 163.225);
106
- --chart-4: oklch(0.508 0.118 165.612);
107
- --chart-5: oklch(0.432 0.095 166.913);
106
+ --ds-background-100: lab(100% 0 0);
107
+ --ds-background-200: lab(98.144% 0 -0.0000119209);
108
+ --ds-gray-100: lab(95.476% -0.0000298023 0);
109
+ --ds-gray-200: lab(93.04% 0.0000298023 -0.0000119209);
110
+ --ds-gray-300: lab(91.3% 0 -0.0000119209);
111
+ --ds-gray-400: lab(92.692% 0 0);
112
+ --ds-gray-500: lab(80.976% 0 -0.0000119209);
113
+ --ds-gray-600: lab(68.912% 0 -0.0000119209);
114
+ --ds-gray-700: lab(59.4% 0 0);
115
+ --ds-gray-800: lab(52.44% 0 0);
116
+ --ds-gray-900: lab(32.72% -0.0000149012 0);
117
+ --ds-gray-1000: lab(7.78201% -0.0000149012 0);
118
+ --ds-gray-alpha-100: lab(0% 0 0 / 0.05);
119
+ --ds-gray-alpha-200: lab(0% 0 0 / 0.081);
120
+ --ds-gray-alpha-300: lab(0% 0 0 / 0.1);
121
+ --ds-gray-alpha-400: lab(0% 0 0 / 0.08);
122
+ --ds-gray-alpha-500: lab(0% 0 0 / 0.21);
123
+ --ds-gray-alpha-600: lab(0% 0 0 / 0.24);
124
+ --ds-gray-alpha-700: lab(0% 0 0 / 0.44);
125
+ --ds-gray-alpha-800: lab(0% 0 0 / 0.51);
126
+ --ds-gray-alpha-900: lab(0% 0 0 / 0.7);
127
+ --ds-gray-alpha-1000: lab(0% 0 0 / 0.91);
128
+ --ds-blue-100: lab(96.8732% -1.39889 -5.07131);
129
+ --ds-blue-200: lab(95.6744% -2.00856 -6.9569);
130
+ --ds-blue-300: lab(93.6801% -3.04809 -10.3604);
131
+ --ds-blue-400: lab(90.2207% -5.88808 -16.0541);
132
+ --ds-blue-500: lab(79.7793% -8.30209 -33.3699);
133
+ --ds-blue-600: lab(68.1823% -8.44929 -52.7934);
134
+ --ds-blue-700: lab(47.9229% 22.3147 -86.8328);
135
+ --ds-blue-800: lab(40.8106% 23.698 -82.9445);
136
+ --ds-blue-900: lab(42.9093% 21.1896 -82.6493);
137
+ --ds-blue-1000: lab(13.8687% 6.36795 -37.4197);
138
+ --ds-red-100: lab(95.6935% 7.32443 2.00332);
139
+ --ds-red-200: lab(94.3497% 9.77078 2.91723);
140
+ --ds-red-300: lab(93.0242% 12.0165 3.78883);
141
+ --ds-red-400: lab(89.6749% 14.9771 6.30434);
142
+ --ds-red-500: lab(80.9124% 32.7639 12.5167);
143
+ --ds-red-600: lab(64.5512% 60.0041 29.3131);
144
+ --ds-red-700: lab(53.9255% 80.6033 48.0845);
145
+ --ds-red-800: lab(48.9343% 78.946 55.0086);
146
+ --ds-red-900: lab(45.3957% 73.7444 51.3924);
147
+ --ds-red-1000: lab(11.6345% 33.5137 13.8317);
148
+ --ds-amber-100: lab(97.2024% 1.01992 12.7992);
149
+ --ds-amber-200: lab(96.5222% 0.44781 19.38);
150
+ --ds-amber-300: lab(95.5532% 0.730604 25.1573);
151
+ --ds-amber-400: lab(89.9281% 6.02812 56.553);
152
+ --ds-amber-500: lab(84.4286% 16.6827 71.0758);
153
+ --ds-amber-600: lab(76.726% 29.5673 105.476);
154
+ --ds-amber-700: lab(78.7256% 26.6905 107.161);
155
+ --ds-amber-800: lab(72.7954% 39.7122 103.301);
156
+ --ds-amber-900: lab(44.3662% 36.3127 70.4646);
157
+ --ds-amber-1000: lab(19.0178% 27.3692 31.8945);
158
+ --ds-green-100: lab(97.5853% -8.11777 6.20661);
159
+ --ds-green-200: lab(96.9166% -10.612 7.60013);
160
+ --ds-green-300: lab(94.6031% -18.6422 15.0889);
161
+ --ds-green-400: lab(91.3796% -27.7496 20.9385);
162
+ --ds-green-500: lab(85.1416% -46.4903 35.8939);
163
+ --ds-green-600: lab(79.6441% -60.3529 50.7117);
164
+ --ds-green-700: lab(61.0339% -50.9714 38.8312);
165
+ --ds-green-800: lab(52.9002% -44.1062 33.0987);
166
+ --ds-green-900: lab(45.7946% -42.7208 32.0975);
167
+ --ds-green-1000: lab(19.2017% -35.6785 27.2939);
168
+ --ds-teal-100: lab(97.7805% -12.1079 -1.6973);
169
+ --ds-teal-200: lab(97.0232% -11.8163 -0.259781);
170
+ --ds-teal-300: lab(94.6978% -16.2983 -0.800061);
171
+ --ds-teal-400: lab(92.4757% -24.5274 -1.99826);
172
+ --ds-teal-500: lab(86.4355% -46.7053 -2.48644);
173
+ --ds-teal-600: lab(80.5746% -56.8254 0.696123);
174
+ --ds-teal-700: lab(61.2492% -55.7738 -2.36303);
175
+ --ds-teal-800: lab(52.4585% -49.4257 -1.83064);
176
+ --ds-teal-900: lab(45.9452% -44.2291 -2.67519);
177
+ --ds-teal-1000: lab(22.2372% -28.1244 -0.0907302);
178
+ --ds-purple-100: lab(95.8064% 5.66289 -6.77673);
179
+ --ds-purple-200: lab(95.9251% 5.05617 -6.57121);
180
+ --ds-purple-300: lab(93.5693% 8.17552 -10.3939);
181
+ --ds-purple-400: lab(89.6505% 14.5975 -16.7056);
182
+ --ds-purple-500: lab(76.2735% 33.6932 -38.7706);
183
+ --ds-purple-600: lab(64.3635% 50.5605 -58.6804);
184
+ --ds-purple-700: lab(42.3587% 81.2683 -86.4789);
185
+ --ds-purple-800: lab(35.0556% 71.1896 -76.2188);
186
+ --ds-purple-900: lab(33.4854% 69.2003 -76.1092);
187
+ --ds-purple-1000: lab(9.18347% 35.0629 -37.5931);
188
+ --ds-pink-100: lab(94.5133% 11.6709 -3.49871);
189
+ --ds-pink-200: lab(94.6026% 10.7321 -1.35195);
190
+ --ds-pink-300: lab(92.2617% 15.1106 -0.957537);
191
+ --ds-pink-400: lab(88.9731% 19.1876 -0.249231);
192
+ --ds-pink-500: lab(80.5902% 30.3695 -1.5003);
193
+ --ds-pink-600: lab(68.2806% 50.7706 0.897086);
194
+ --ds-pink-700: lab(54.7091% 76.7064 2.85504);
195
+ --ds-pink-800: lab(50.1788% 75.5319 8.12644);
196
+ --ds-pink-900: lab(43.5237% 66.4129 5.12512);
197
+ --ds-pink-1000: lab(12.9204% 31.3916 -0.183001);
198
+
199
+ --background: var(--ds-background-200);
200
+ --foreground: var(--ds-gray-1000);
201
+ --card: var(--ds-background-100);
202
+ --card-foreground: var(--ds-gray-1000);
203
+ --popover: var(--ds-background-100);
204
+ --popover-foreground: var(--ds-gray-1000);
205
+ --primary: var(--ds-gray-1000);
206
+ --primary-foreground: var(--ds-background-100);
207
+ --secondary: var(--ds-gray-200);
208
+ --secondary-foreground: var(--ds-gray-1000);
209
+ --muted: var(--ds-gray-100);
210
+ --muted-foreground: var(--ds-gray-900);
211
+ --placeholder: var(--ds-gray-700);
212
+ --accent: var(--ds-gray-100);
213
+ --accent-foreground: var(--ds-gray-1000);
214
+ --destructive: var(--ds-red-900);
215
+ --success: var(--ds-green-900);
216
+ --warning: var(--ds-amber-900);
217
+ --warning-foreground: var(--ds-amber-900);
218
+ --border: var(--ds-gray-400);
219
+ --highlight: var(--ds-gray-600);
220
+ --input: var(--ds-gray-400);
221
+ --ring: var(--ds-gray-600);
222
+ --chart-1: var(--ds-blue-700);
223
+ --chart-2: var(--ds-purple-700);
224
+ --chart-3: var(--ds-teal-700);
225
+ --chart-4: var(--ds-amber-700);
226
+ --chart-5: var(--ds-pink-700);
108
227
  --radius: 0.625rem;
109
228
  --shadow-background-border: 0 0 0 1px var(--background);
110
- --shadow-border-base: 0 0 0 1px
111
- color-mix(in oklab, var(--border) 60%, transparent);
112
- --shadow-border-inset: inset 0 0 0 1px var(--border);
229
+ --shadow-border-base: 0 0 0 1px var(--ds-gray-alpha-400);
230
+ --shadow-border-inset: inset 0 0 0 1px var(--ds-gray-alpha-400);
113
231
  --shadow-border: var(--shadow-border-base), var(--shadow-background-border);
114
232
  --shadow-small: 0px 2px 2px #0000000a;
115
233
  --shadow-border-small:
@@ -141,16 +259,14 @@
141
259
  var(--shadow-border-base), 0px 1px 1px #00000005, 0px 8px 16px -4px #0000000a, 0px 24px 32px -8px #0000000f, var(
142
260
  --shadow-background-border
143
261
  );
144
- --sidebar: oklch(0.983 0 0);
145
- --sidebar-foreground: oklch(0.3 0 0);
146
- --sidebar-primary: oklch(0.15 0 0);
147
- --sidebar-primary-foreground: oklch(0.985 0 0);
148
- --sidebar-accent: oklch(0.95 0 0);
149
- --sidebar-accent-foreground: oklch(0 0 0);
150
- --sidebar-border: oklch(0.95 0 0);
151
- --sidebar-ring: oklch(0.708 0 0);
152
- --background: oklch(0.983 0 0);
153
- --foreground: oklch(0.145 0 0);
262
+ --sidebar: var(--ds-background-200);
263
+ --sidebar-foreground: var(--ds-gray-900);
264
+ --sidebar-primary: var(--ds-gray-1000);
265
+ --sidebar-primary-foreground: var(--ds-background-100);
266
+ --sidebar-accent: var(--ds-gray-200);
267
+ --sidebar-accent-foreground: var(--ds-gray-1000);
268
+ --sidebar-border: var(--ds-gray-400);
269
+ --sidebar-ring: var(--ds-gray-600);
154
270
  --tt-bg-color: var(--background);
155
271
  --tt-bg-color-contrast: var(--border);
156
272
  --tt-color-highlight-yellow: #fef9c3;
@@ -441,7 +557,7 @@
441
557
  .admin-root
442
558
  .content-editor-prose
443
559
  .is-empty[data-placeholder]:has(> .ProseMirror-trailingBreak)::before {
444
- @apply pointer-events-none absolute inset-x-0 h-0 w-full text-muted-foreground;
560
+ @apply pointer-events-none absolute inset-x-0 h-0 w-full text-placeholder;
445
561
  text-align: inherit;
446
562
  }
447
563
 
@@ -3,7 +3,37 @@
3
3
  import type * as React from 'react'
4
4
  import { cn } from '@admin/utils/shared/cn'
5
5
 
6
- function DataGridRow({ className, style, ...props }: React.ComponentProps<'div'>) {
6
+ const INTERACTIVE_ROW_SELECTOR = 'button, a, input, [role="checkbox"], [role="switch"]'
7
+
8
+ type DataGridRowProps = React.ComponentProps<'div'> & {
9
+ onActivate?: () => void
10
+ }
11
+
12
+ function DataGridRow({
13
+ className,
14
+ style,
15
+ onActivate,
16
+ onClick,
17
+ onKeyDown,
18
+ ...props
19
+ }: DataGridRowProps) {
20
+ function handleClick(event: React.MouseEvent<HTMLDivElement>) {
21
+ onClick?.(event)
22
+ const target = event.target
23
+ if (!onActivate || !(target instanceof HTMLElement)) return
24
+ if (!event.currentTarget.contains(target)) return
25
+ if (target.closest(INTERACTIVE_ROW_SELECTOR)) return
26
+ onActivate()
27
+ }
28
+
29
+ function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
30
+ onKeyDown?.(event)
31
+ if (!onActivate || event.target !== event.currentTarget) return
32
+ if (event.key !== 'Enter' && event.key !== ' ') return
33
+ event.preventDefault()
34
+ onActivate()
35
+ }
36
+
7
37
  return (
8
38
  <div
9
39
  role="row"
@@ -14,6 +44,8 @@ function DataGridRow({ className, style, ...props }: React.ComponentProps<'div'>
14
44
  )}
15
45
  style={{ ...style, gridColumn: '1 / -1', gridTemplateColumns: 'subgrid' }}
16
46
  tabIndex={-1}
47
+ onClick={handleClick}
48
+ onKeyDown={handleKeyDown}
17
49
  {...props}
18
50
  />
19
51
  )
@@ -99,7 +99,7 @@ export function MediaGalleryDialog({
99
99
  onOpenChange(next)
100
100
  }}
101
101
  >
102
- <DialogContent className="flex h-[80svh] max-h-[700px] min-h-0 max-w-3xl flex-col gap-0 overflow-hidden">
102
+ <DialogContent className="flex h-[80svh] max-h-media-dialog min-h-0 max-w-3xl flex-col gap-0 overflow-hidden">
103
103
  <DialogHeader>
104
104
  <DialogTitle>Select file</DialogTitle>
105
105
  <DialogDescription>
@@ -128,7 +128,7 @@ export function MediaGalleryDialog({
128
128
  progress={progress}
129
129
  isUploading={isUploading}
130
130
  accept={accept}
131
- className="h-[180px]"
131
+ className="h-media-tile"
132
132
  />
133
133
  </TabsUi.TabsContent>
134
134
 
@@ -136,7 +136,7 @@ export function MediaGalleryDialog({
136
136
  <MediaUrlImporter
137
137
  onImportFromUrl={uploadFromUrl}
138
138
  accept={accept}
139
- className="h-[180px]"
139
+ className="h-media-tile"
140
140
  />
141
141
  </TabsUi.TabsContent>
142
142
  </TabsUi.Tabs>
@@ -12,6 +12,8 @@ interface MediaPreviewProps {
12
12
  showMetadata?: boolean
13
13
  }
14
14
 
15
+ const MEDIA_PREVIEW_IMAGE_SIZES = '200px'
16
+
15
17
  export function MediaPreview({ media, isFilled = false, showMetadata = false }: MediaPreviewProps) {
16
18
  const { hasImageFailed, markImageFailed } = useFailedImageUrl(media.url)
17
19
 
@@ -26,7 +28,7 @@ export function MediaPreview({ media, isFilled = false, showMetadata = false }:
26
28
  'object-contain': !isFilled
27
29
  })}
28
30
  fill
29
- sizes="200px"
31
+ sizes={MEDIA_PREVIEW_IMAGE_SIZES}
30
32
  unoptimized
31
33
  loading="lazy"
32
34
  decoding="async"
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import type { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form'
4
+ import { useWatch } from 'react-hook-form'
5
+
6
+ function hasValue(value: unknown): boolean {
7
+ if (value === null || value === undefined) return false
8
+ if (typeof value === 'string') return value.trim().length > 0
9
+ if (Array.isArray(value)) return value.length > 0
10
+
11
+ return true
12
+ }
13
+
14
+ export function useFieldErrorVisible<TValues extends FieldValues>(
15
+ form: UseFormReturn<TValues>,
16
+ name: FieldPath<TValues>
17
+ ): boolean {
18
+ const value = useWatch({ control: form.control, name })
19
+ const { error, isTouched } = form.getFieldState(name, form.formState)
20
+
21
+ if (!error) return false
22
+ if (form.formState.isSubmitted) return true
23
+
24
+ return isTouched && hasValue(value)
25
+ }
@@ -69,7 +69,7 @@ export function useMediaSelection(mode: 'single' | 'multi') {
69
69
  [mode]
70
70
  )
71
71
 
72
- const reset = React.useCallback(() => setSelectedMap(new Map()), [])
72
+ const reset = () => setSelectedMap(emptySelection())
73
73
 
74
74
  const getSelection = React.useCallback(() => {
75
75
  const values = [...selectedMap.values()]
@@ -190,11 +190,17 @@ export function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
190
190
  formData.append('allowedTypes', validationConfig.allowedTypes.join(','))
191
191
  }
192
192
 
193
- const response = await fetch('/api/admin/upload', {
194
- method: 'POST',
195
- body: formData,
196
- signal: abortController.signal
197
- })
193
+ let response: Response
194
+ try {
195
+ response = await fetch('/api/admin/upload', {
196
+ method: 'POST',
197
+ body: formData,
198
+ signal: abortController.signal
199
+ })
200
+ } catch (error) {
201
+ if (error instanceof Error && error.name === 'AbortError') throw error
202
+ throw new Error('Upload failed. Check your connection and try again.')
203
+ }
198
204
 
199
205
  const result = (await response.json()) as UploadFileResult
200
206
 
@@ -51,6 +51,7 @@ export async function cleanupAuditLogs(): Promise<CleanupAuditLogsResult> {
51
51
 
52
52
  return { success: true, deletedCount: deletedRows.length, cutoff }
53
53
  } catch (error) {
54
+ console.error('Failed to clean up audit logs:', error)
54
55
  return {
55
56
  success: false,
56
57
  error: toActionError(error, 'Failed to clean up audit logs')
@@ -1,4 +1,4 @@
1
- import type { AuditLogEntry, AuditLogPage, AuditLogQuery } from '@admin/actions/audit/types'
1
+ import type { AuditLogPage, AuditLogQuery } from '@admin/actions/audit/types'
2
2
  import { clampPage } from '@admin/actions/audit/clamp-page'
3
3
  import { getAuditLogOrderBy } from '@admin/actions/audit/get-audit-log-order-by'
4
4
  import { normalizePageSize } from '@admin/actions/audit/normalize-page-size'
@@ -62,7 +62,7 @@ export async function queryAuditLogs(query: AuditLogQuery = {}): Promise<AuditLo
62
62
  const resolvedPageSize = pageSize ?? total
63
63
 
64
64
  return {
65
- auditLogs: rows as AuditLogEntry[],
65
+ auditLogs: rows,
66
66
  total,
67
67
  page,
68
68
  pageSize: resolvedPageSize,
@@ -6,7 +6,6 @@ import db from '@admin/db'
6
6
  import { auditLog } from '@admin/db/schema'
7
7
 
8
8
  const MAX_TARGET_TYPE_LENGTH = 100
9
- const MAX_SOURCE_TYPE_LENGTH = 50
10
9
 
11
10
  function truncate(value: string | null | undefined, maxLength: number): string | null {
12
11
  if (!value) return null
@@ -24,7 +23,7 @@ export async function recordAuditEvent(event: AdminEventContext): Promise<void>
24
23
  actorEmail: event.user?.email ?? null,
25
24
  actorName: event.user?.name ?? null,
26
25
  actorRole: typeof event.user?.role === 'string' ? event.user.role : null,
27
- sourceType: truncate(event.sourceType, MAX_SOURCE_TYPE_LENGTH) ?? 'server_action',
26
+ sourceType: event.sourceType ?? 'server_action',
28
27
  ipAddress: truncate(event.ipAddress, MAX_IP_LENGTH),
29
28
  userAgent: truncate(event.userAgent, MAX_USER_AGENT_LENGTH),
30
29
  changedFields: normalizeChangedFields(event.changedFields),
@@ -1,12 +1,15 @@
1
1
  'use client'
2
2
 
3
+ import type { auth } from '@admin/auth/server'
4
+ import { inferAdditionalFields } from 'better-auth/client/plugins'
3
5
  import { createAuthClient } from 'better-auth/react'
4
6
 
5
7
  export const authClient = createAuthClient({
6
8
  baseURL:
7
9
  process.env.NEXT_PUBLIC_BETTERSTART_AUTH_URL ||
8
10
  (typeof window !== 'undefined' ? window.location.origin : ''),
9
- basePath: '/api/admin/auth'
11
+ basePath: '/api/admin/auth',
12
+ plugins: [inferAdditionalFields<typeof auth>()]
10
13
  })
11
14
 
12
15
  export const { signIn, signUp, signOut, useSession } = authClient
@@ -3,6 +3,7 @@
3
3
  import type { EntityVersionWithAuthor } from './types'
4
4
  import db from '@admin/db'
5
5
  import { entityVersions, user } from '@admin/db/schema'
6
+ import { isRecord } from '@admin/utils/shared/is-record'
6
7
  import { and, desc, eq } from 'drizzle-orm'
7
8
  import { alias } from 'drizzle-orm/pg-core'
8
9
  import { cacheLife, cacheTag } from 'next/cache'
@@ -40,7 +41,10 @@ export async function getEntityVersions(
40
41
  .where(and(eq(entityVersions.entityType, entityType), eq(entityVersions.entityId, entityId)))
41
42
  .orderBy(desc(entityVersions.version), desc(entityVersions.createdAt))
42
43
 
43
- return rows as EntityVersionWithAuthor[]
44
+ return rows.map((row) => ({
45
+ ...row,
46
+ data: isRecord(row.data) ? row.data : {}
47
+ }))
44
48
  } catch (error) {
45
49
  console.error('Error fetching entity versions:', error)
46
50
  return []
@@ -13,7 +13,7 @@ export async function getAllFormSettings(): Promise<FormSettingsData[]> {
13
13
 
14
14
  try {
15
15
  const settings = await db.select().from(formSettings)
16
- return settings as FormSettingsData[]
16
+ return settings
17
17
  } catch (error) {
18
18
  console.error('Error fetching all form settings:', error)
19
19
  return []
@@ -43,7 +43,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
43
43
  user: actor,
44
44
  timestamp: now,
45
45
  sourceType: 'admin_ui' as const,
46
- changedFields: getTouchedFieldNames(data as unknown as Record<string, unknown>),
46
+ changedFields: getTouchedFieldNames(data),
47
47
  metadata: { contentType: media.contentType, size: media.size },
48
48
  ...auditRequestMetadata
49
49
  })
@@ -52,6 +52,7 @@ export async function createMedia(data: CreateMediaInput): Promise<CreateMediaRe
52
52
  ? { success: true, media }
53
53
  : { success: false, error: 'Failed to create media record' }
54
54
  } catch (error) {
55
+ console.error('Failed to create media record:', error)
55
56
  return {
56
57
  success: false,
57
58
  error: toActionError(error, 'Failed to create media record')
@@ -45,6 +45,7 @@ export async function deleteMediaBulk(ids: string[]): Promise<DeleteMediaBulkRes
45
45
  }
46
46
  return { success: true, deletedCount: deletedRows.length }
47
47
  } catch (error) {
48
+ console.error('Failed to delete media:', error)
48
49
  return {
49
50
  success: false,
50
51
  error: toActionError(error, 'Failed to delete media')
@@ -36,6 +36,7 @@ export async function deleteMedia(id: string): Promise<DeleteMediaResult> {
36
36
  })
37
37
  return { success: true }
38
38
  } catch (error) {
39
+ console.error('Failed to delete media:', error)
39
40
  return {
40
41
  success: false,
41
42
  error: toActionError(error, 'Failed to delete media')
@@ -41,15 +41,13 @@ export async function updateMedia(
41
41
  user: actor,
42
42
  timestamp: now,
43
43
  sourceType: 'admin_ui' as const,
44
- changedFields: getChangedFieldNames(
45
- previousMedia as unknown as Record<string, unknown>,
46
- data as Record<string, unknown>
47
- ),
44
+ changedFields: getChangedFieldNames(previousMedia, data),
48
45
  metadata: { contentType: previousMedia?.contentType ?? null },
49
46
  ...auditRequestMetadata
50
47
  })
51
48
  return { success: true }
52
49
  } catch (error) {
50
+ console.error('Failed to update media:', error)
53
51
  return {
54
52
  success: false,
55
53
  error: toActionError(error, 'Failed to update media')
@@ -52,6 +52,7 @@ export async function changeOwnPassword(
52
52
 
53
53
  return { success: true }
54
54
  } catch (error) {
55
+ console.error('Failed to change password:', error)
55
56
  return {
56
57
  success: false,
57
58
  error: toActionError(error, 'Failed to change password')
@@ -67,6 +67,7 @@ export async function updateEmail(
67
67
 
68
68
  return { success: true }
69
69
  } catch (error) {
70
+ console.error('Failed to update email:', error)
70
71
  return {
71
72
  success: false,
72
73
  error: toActionError(error, 'Failed to update email')
@@ -90,6 +90,7 @@ export async function uploadFiles(
90
90
 
91
91
  return { success: true, files: uploadedFiles }
92
92
  } catch (error) {
93
+ console.error('Failed to upload files:', error)
93
94
  return { success: false, error: toActionError(error, 'Failed to upload files') }
94
95
  }
95
96
  }
@@ -22,12 +22,21 @@ export async function uploadMediaFromUrl(
22
22
  return { success: false, error: 'Only HTTP and HTTPS URLs are allowed' }
23
23
  }
24
24
 
25
- const response = await fetch(mediaUrl, {
26
- headers: {
27
- 'User-Agent': 'Mozilla/5.0 (compatible; BetterStartMediaFetcher/1.0)'
28
- },
29
- signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS)
30
- })
25
+ let response: Response
26
+ try {
27
+ response = await fetch(mediaUrl, {
28
+ headers: {
29
+ 'User-Agent': 'Mozilla/5.0 (compatible; BetterStartMediaFetcher/1.0)'
30
+ },
31
+ signal: AbortSignal.timeout(REMOTE_FETCH_TIMEOUT_MS)
32
+ })
33
+ } catch (error) {
34
+ console.error('Failed to fetch remote media:', error)
35
+ return {
36
+ success: false,
37
+ error: "Couldn't reach that URL. Check the link and try again."
38
+ }
39
+ }
31
40
 
32
41
  if (!response.ok) {
33
42
  return {
@@ -129,6 +138,7 @@ export async function uploadMediaFromUrl(
129
138
  ]
130
139
  }
131
140
  } catch (error) {
141
+ console.error('Failed to upload media from URL:', error)
132
142
  return { success: false, error: toActionError(error, 'Failed to upload media from URL') }
133
143
  }
134
144
  }
@@ -70,6 +70,7 @@ export async function changeUserPassword(
70
70
 
71
71
  return { success: true }
72
72
  } catch (error) {
73
+ console.error('Failed to change user password:', error)
73
74
  return {
74
75
  success: false,
75
76
  error: toActionError(error, 'Failed to change user password')
@@ -48,7 +48,7 @@ export async function createUser(input: CreateUserInput): Promise<CreateUserResu
48
48
  user: actor,
49
49
  timestamp: now,
50
50
  sourceType: 'admin_ui' as const,
51
- changedFields: getTouchedFieldNames(input as unknown as Record<string, unknown>),
51
+ changedFields: getTouchedFieldNames(input),
52
52
  metadata: { role: input.role },
53
53
  ...auditRequestMetadata
54
54
  })
@@ -66,6 +66,7 @@ export async function createUser(input: CreateUserInput): Promise<CreateUserResu
66
66
  }
67
67
  }
68
68
  } catch (error) {
69
+ console.error('Failed to create user:', error)
69
70
  return {
70
71
  success: false,
71
72
  error: toActionError(error, 'Failed to create user')
@@ -39,6 +39,7 @@ export async function deleteUser(userId: string): Promise<DeleteUserResult> {
39
39
  })
40
40
  return { success: true }
41
41
  } catch (error) {
42
+ console.error('Failed to delete user:', error)
42
43
  return {
43
44
  success: false,
44
45
  error: toActionError(error, 'Failed to delete user')
@@ -15,7 +15,7 @@ export interface UsersPage {
15
15
  total: number
16
16
  }
17
17
 
18
- export interface CreateUserInput {
18
+ export type CreateUserInput = {
19
19
  email: string
20
20
  name: string
21
21
  password: string
@@ -43,6 +43,7 @@ export async function updateUserRole(
43
43
 
44
44
  return { success: true }
45
45
  } catch (error) {
46
+ console.error('Failed to update user role:', error)
46
47
  return {
47
48
  success: false,
48
49
  error: toActionError(error, 'Failed to update user role')