appflare 0.2.25 → 0.2.27

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 (140) hide show
  1. package/Documentation.md +758 -758
  2. package/cli/commands/index.ts +238 -238
  3. package/cli/generate.ts +178 -178
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1183 -1183
  7. package/cli/templates/auth/README.md +156 -156
  8. package/cli/templates/auth/config.ts +61 -61
  9. package/cli/templates/auth/route-config.ts +1 -1
  10. package/cli/templates/auth/route-handler.ts +1 -1
  11. package/cli/templates/auth/route-request-utils.ts +5 -5
  12. package/cli/templates/auth/route.config.ts +18 -18
  13. package/cli/templates/auth/route.handler.ts +18 -18
  14. package/cli/templates/auth/route.request-utils.ts +55 -55
  15. package/cli/templates/auth/route.ts +14 -14
  16. package/cli/templates/core/README.md +266 -266
  17. package/cli/templates/core/app-creation.ts +19 -19
  18. package/cli/templates/core/client/appflare.ts +112 -112
  19. package/cli/templates/core/client/handlers/index.ts +748 -748
  20. package/cli/templates/core/client/handlers.ts +1 -1
  21. package/cli/templates/core/client/index.ts +7 -7
  22. package/cli/templates/core/client/storage.ts +205 -180
  23. package/cli/templates/core/client/types.ts +186 -184
  24. package/cli/templates/core/client-modules/appflare.ts +1 -1
  25. package/cli/templates/core/client-modules/handlers.ts +1 -1
  26. package/cli/templates/core/client-modules/index.ts +1 -1
  27. package/cli/templates/core/client-modules/storage.ts +1 -1
  28. package/cli/templates/core/client-modules/types.ts +1 -1
  29. package/cli/templates/core/client.artifacts.ts +39 -39
  30. package/cli/templates/core/client.ts +4 -4
  31. package/cli/templates/core/drizzle.ts +15 -15
  32. package/cli/templates/core/export.ts +14 -14
  33. package/cli/templates/core/handlers.route.ts +24 -24
  34. package/cli/templates/core/handlers.ts +1 -1
  35. package/cli/templates/core/imports.ts +9 -9
  36. package/cli/templates/core/server.ts +38 -38
  37. package/cli/templates/core/types.ts +6 -6
  38. package/cli/templates/core/wrangler.ts +109 -109
  39. package/cli/templates/dashboard/builders/functions/index.ts +17 -17
  40. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
  41. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
  42. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +171 -171
  43. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
  44. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +554 -554
  45. package/cli/templates/dashboard/builders/navigation.ts +122 -122
  46. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  47. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  48. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  49. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  50. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  51. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  52. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  53. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  55. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  56. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  57. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  58. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  59. package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
  60. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  61. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  62. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  63. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  64. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  65. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  66. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  67. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  68. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  69. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  70. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  71. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  72. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  73. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  74. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  75. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  76. package/cli/templates/dashboard/components/layout.ts +388 -388
  77. package/cli/templates/dashboard/components/login-page.ts +65 -65
  78. package/cli/templates/dashboard/index.ts +61 -61
  79. package/cli/templates/dashboard/types.ts +9 -9
  80. package/cli/templates/handlers/README.md +353 -353
  81. package/cli/templates/handlers/auth.ts +37 -37
  82. package/cli/templates/handlers/execution.ts +42 -42
  83. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  84. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  85. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  86. package/cli/templates/handlers/generators/context/storage-api.ts +82 -112
  87. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  88. package/cli/templates/handlers/generators/context/types.ts +18 -18
  89. package/cli/templates/handlers/generators/context.ts +43 -43
  90. package/cli/templates/handlers/generators/execution.ts +15 -15
  91. package/cli/templates/handlers/generators/handlers.ts +13 -13
  92. package/cli/templates/handlers/generators/registration/modules/cron.ts +26 -26
  93. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  94. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  95. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  96. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  97. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  98. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  99. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +516 -516
  100. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -56
  101. package/cli/templates/handlers/generators/registration/modules/storage.ts +192 -194
  102. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  103. package/cli/templates/handlers/generators/types/context.ts +67 -66
  104. package/cli/templates/handlers/generators/types/core.ts +106 -106
  105. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  106. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +259 -259
  107. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  108. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1031 -1031
  109. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +246 -246
  110. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  111. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +121 -121
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +676 -676
  116. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  117. package/cli/templates/handlers/index.ts +43 -43
  118. package/cli/templates/handlers/operations.ts +116 -116
  119. package/cli/templates/handlers/registration.ts +91 -91
  120. package/cli/templates/handlers/types.ts +15 -15
  121. package/cli/templates/handlers/utils.ts +48 -48
  122. package/cli/types.ts +110 -110
  123. package/cli/utils/handler-discovery.ts +466 -466
  124. package/cli/utils/json-utils.ts +24 -24
  125. package/cli/utils/path-utils.ts +19 -19
  126. package/cli/utils/schema-discovery.ts +399 -399
  127. package/dist/cli/index.js +95 -99
  128. package/dist/cli/index.mjs +95 -99
  129. package/index.ts +18 -18
  130. package/package.json +58 -58
  131. package/react/index.ts +5 -5
  132. package/react/use-infinite-query.ts +252 -252
  133. package/react/use-mutation.ts +89 -89
  134. package/react/use-query.ts +207 -207
  135. package/schema.ts +415 -415
  136. package/test-better-auth-hash.ts +2 -2
  137. package/tsconfig.json +6 -6
  138. package/tsup.config.ts +82 -82
  139. package/dist/cli/index.d.mts +0 -2
  140. package/dist/cli/index.d.ts +0 -2
@@ -1,291 +1,291 @@
1
- import { DiscoveredTable } from "../../../types";
2
- import { buildPaginationHtml } from "../common/pagination";
3
- import { buildSearchBarHtml } from "../common/search-bar";
4
-
5
- /**
6
- * Builds the GET route handler code string for /admin/table/:tableName
7
- * PocketBase-style layout with breadcrumb header, filter bar, and clean table.
8
- */
9
- export function buildTableGetRoute(
10
- table: DiscoveredTable,
11
- defaultSort: string,
12
- primaryKey: string,
13
- hasPrimaryKey: boolean,
14
- columns: string[],
15
- searchConditions: string,
16
- headers: string,
17
- rowCells: string,
18
- actionsCell: string,
19
- createInputs: string,
20
- ): string {
21
- const paginationHtml = buildPaginationHtml(
22
- `/admin/table/${table.exportName}`,
23
- );
24
- const searchBarHtml = buildSearchBarHtml(
25
- `/admin/table/${table.exportName}`,
26
- "Search term or filter...",
27
- );
28
- const headerSelectionCell = hasPrimaryKey
29
- ? `<th class="w-10"><input id="select-all-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" /></th>`
30
- : `<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>`;
31
- const rowSelectionCell = hasPrimaryKey
32
- ? `<td><input type="checkbox" class="checkbox checkbox-xs row-select-checkbox" value="\${String((row as any).${primaryKey} ?? '')}" /></td>`
33
- : `<td><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></td>`;
34
- const bulkDeleteUi = hasPrimaryKey
35
- ? `
36
- \t\t\t\t\t\t<div id="bulk-delete-bar-${table.exportName}" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 hidden">
37
- \t\t\t\t\t\t\t<div class="bg-base-100 border border-base-200 rounded-xl shadow-lg px-3 py-2 flex items-center gap-3">
38
- \t\t\t\t\t\t\t\t<div class="text-xs text-base-content/70">
39
- \t\t\t\t\t\t\t\t\t<span id="bulk-selected-count-${table.exportName}" class="font-medium text-base-content">0</span> selected
40
- \t\t\t\t\t\t\t\t</div>
41
- \t\t\t\t\t\t\t\t<label class="label cursor-pointer gap-2 py-0 px-1">
42
- \t\t\t\t\t\t\t\t\t<span class="label-text text-xs">All matching (\${total})</span>
43
- \t\t\t\t\t\t\t\t\t<input id="bulk-all-matching-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" />
44
- \t\t\t\t\t\t\t\t</label>
45
- \t\t\t\t\t\t\t\t<button id="bulk-delete-trigger-${table.exportName}" type="button" class="btn btn-error btn-xs gap-1">
46
- \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:delete-outline" width="14" height="14"></iconify-icon>
47
- \t\t\t\t\t\t\t\t\tDelete selected
48
- \t\t\t\t\t\t\t\t</button>
49
- \t\t\t\t\t\t\t</div>
50
- \t\t\t\t\t\t</div>
51
-
52
- \t\t\t\t\t\t<dialog id="bulk-delete-modal-${table.exportName}" class="modal">
53
- \t\t\t\t\t\t\t<div class="modal-box max-w-sm p-6 space-y-4">
54
- \t\t\t\t\t\t\t\t<h3 class="font-semibold text-base">Delete selected rows?</h3>
55
- \t\t\t\t\t\t\t\t<p class="text-sm text-base-content/70" id="bulk-delete-description-${table.exportName}">This action cannot be undone.</p>
56
- \t\t\t\t\t\t\t\t<form id="bulk-delete-form-${table.exportName}" hx-post="/admin/table/${table.exportName}/delete-bulk" hx-target="#main-content" hx-swap="outerHTML" class="space-y-3">
57
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="bulkMode" id="bulk-delete-mode-${table.exportName}" value="selected" />
58
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="selectedIds" id="bulk-delete-ids-${table.exportName}" value="" />
59
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
60
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
61
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
62
- \t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
63
- \t\t\t\t\t\t\t\t\t<div class="flex justify-end gap-2 pt-1">
64
- \t\t\t\t\t\t\t\t\t\t<button type="button" class="btn btn-ghost btn-sm" onclick="this.closest('dialog')?.close()">Cancel</button>
65
- \t\t\t\t\t\t\t\t\t\t<button type="submit" class="btn btn-error btn-sm">Delete</button>
66
- \t\t\t\t\t\t\t\t\t</div>
67
- \t\t\t\t\t\t\t\t</form>
68
- \t\t\t\t\t\t\t</div>
69
- \t\t\t\t\t\t\t<form method="dialog" class="modal-backdrop"><button>close</button></form>
70
- \t\t\t\t\t\t</dialog>
71
-
72
- \t\t\t\t\t\t<script>
73
- \t\t\t\t\t\t\t(() => {
74
- \t\t\t\t\t\t\t\tconst container = document.querySelector('#main-content');
75
- \t\t\t\t\t\t\t\tif (!container) return;
76
-
77
- \t\t\t\t\t\t\t\tconst rowCheckboxes = Array.from(container.querySelectorAll('.row-select-checkbox')).filter((node) => node instanceof HTMLInputElement);
78
- \t\t\t\t\t\t\t\tif (rowCheckboxes.length === 0) return;
79
-
80
- \t\t\t\t\t\t\t\tconst selectAll = container.querySelector('#select-all-${table.exportName}');
81
- \t\t\t\t\t\t\t\tconst bar = container.querySelector('#bulk-delete-bar-${table.exportName}');
82
- \t\t\t\t\t\t\t\tconst selectedCount = container.querySelector('#bulk-selected-count-${table.exportName}');
83
- \t\t\t\t\t\t\t\tconst allMatching = container.querySelector('#bulk-all-matching-${table.exportName}');
84
- \t\t\t\t\t\t\t\tconst trigger = container.querySelector('#bulk-delete-trigger-${table.exportName}');
85
- \t\t\t\t\t\t\t\tconst modal = container.querySelector('#bulk-delete-modal-${table.exportName}');
86
- \t\t\t\t\t\t\t\tconst modeInput = container.querySelector('#bulk-delete-mode-${table.exportName}');
87
- \t\t\t\t\t\t\t\tconst idsInput = container.querySelector('#bulk-delete-ids-${table.exportName}');
88
- \t\t\t\t\t\t\t\tconst description = container.querySelector('#bulk-delete-description-${table.exportName}');
89
-
90
- \t\t\t\t\t\t\t\tif (!(selectAll instanceof HTMLInputElement) ||
91
- \t\t\t\t\t\t\t\t\t!(bar instanceof HTMLElement) ||
92
- \t\t\t\t\t\t\t\t\t!(selectedCount instanceof HTMLElement) ||
93
- \t\t\t\t\t\t\t\t\t!(allMatching instanceof HTMLInputElement) ||
94
- \t\t\t\t\t\t\t\t\t!(trigger instanceof HTMLButtonElement) ||
95
- \t\t\t\t\t\t\t\t\t!(modal instanceof HTMLDialogElement) ||
96
- \t\t\t\t\t\t\t\t\t!(modeInput instanceof HTMLInputElement) ||
97
- \t\t\t\t\t\t\t\t\t!(idsInput instanceof HTMLInputElement) ||
98
- \t\t\t\t\t\t\t\t\t!(description instanceof HTMLElement)) {
99
- \t\t\t\t\t\t\t\t\treturn;
100
- \t\t\t\t\t\t\t\t}
101
-
102
- \t\t\t\t\t\t\t\tconst totalRows = Number(\${total});
103
- \t\t\t\t\t\t\t\tconst getSelectedIds = () => rowCheckboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value).filter((value) => value.length > 0);
104
-
105
- \t\t\t\t\t\t\t\tconst updateUi = () => {
106
- \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
107
- \t\t\t\t\t\t\t\t\tconst selected = allMatching.checked ? totalRows : ids.length;
108
- \t\t\t\t\t\t\t\t\tselectedCount.textContent = String(selected);
109
- \t\t\t\t\t\t\t\t\tbar.classList.toggle('hidden', selected === 0);
110
- \t\t\t\t\t\t\t\t\tconst checkedCount = ids.length;
111
- \t\t\t\t\t\t\t\t\tselectAll.checked = checkedCount > 0 && checkedCount === rowCheckboxes.length;
112
- \t\t\t\t\t\t\t\t\tselectAll.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
113
- \t\t\t\t\t\t\t\t};
114
-
115
- \t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
116
- \t\t\t\t\t\t\t\t\tcheckbox.addEventListener('change', () => {
117
- \t\t\t\t\t\t\t\t\t\tif (allMatching.checked) allMatching.checked = false;
118
- \t\t\t\t\t\t\t\t\t\tupdateUi();
119
- \t\t\t\t\t\t\t\t\t});
120
- \t\t\t\t\t\t\t\t});
121
-
122
- \t\t\t\t\t\t\t\tselectAll.addEventListener('change', () => {
123
- \t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
124
- \t\t\t\t\t\t\t\t\t\tcheckbox.checked = selectAll.checked;
125
- \t\t\t\t\t\t\t\t\t});
126
- \t\t\t\t\t\t\t\t\tif (allMatching.checked && !selectAll.checked) allMatching.checked = false;
127
- \t\t\t\t\t\t\t\t\tupdateUi();
128
- \t\t\t\t\t\t\t\t});
129
-
130
- \t\t\t\t\t\t\t\tallMatching.addEventListener('change', () => {
131
- \t\t\t\t\t\t\t\t\tif (allMatching.checked) {
132
- \t\t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
133
- \t\t\t\t\t\t\t\t\t\t\tcheckbox.checked = true;
134
- \t\t\t\t\t\t\t\t\t\t});
135
- \t\t\t\t\t\t\t\t\t}
136
- \t\t\t\t\t\t\t\t\tupdateUi();
137
- \t\t\t\t\t\t\t\t});
138
-
139
- \t\t\t\t\t\t\t\ttrigger.addEventListener('click', () => {
140
- \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
141
- \t\t\t\t\t\t\t\t\tif (!allMatching.checked && ids.length === 0) return;
142
- \t\t\t\t\t\t\t\t\tmodeInput.value = allMatching.checked ? 'all-matching' : 'selected';
143
- \t\t\t\t\t\t\t\t\tidsInput.value = ids.join(',');
144
- \t\t\t\t\t\t\t\t\tdescription.textContent = allMatching.checked
145
- \t\t\t\t\t\t\t\t\t\t? 'This will permanently delete all rows matching the current search context.'
146
- \t\t\t\t\t\t\t\t\t\t: 'This action cannot be undone.';
147
- \t\t\t\t\t\t\t\t\tmodal.showModal();
148
- \t\t\t\t\t\t\t\t});
149
-
150
- \t\t\t\t\t\t\t\tupdateUi();
151
- \t\t\t\t\t\t\t})();
152
- \t\t\t\t\t\t</script>
153
- `
154
- : "";
155
-
156
- return `
157
- \tadminApp.get('/table/${table.exportName}', async (c) => {
158
- \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
159
- \t\tconst page = parseInt(c.req.query('page') || '1');
160
- \t\tconst limit = 20;
161
- \t\tconst offset = (page - 1) * limit;
162
- \t\tconst sort = c.req.query('sort') || '${defaultSort}';
163
- \t\tconst order = c.req.query('order') || 'desc';
164
- \t\tconst search = c.req.query('search') || '';
165
-
166
- \t\tlet tableSchema = (schema as any).${table.exportName};
167
- \t\tif (!tableSchema) return c.text("Table missing", 404);
168
-
169
- \t\tlet query = db.select().from(tableSchema);
170
- \t\tlet countQuery = db.select({ count: sql\`count(*)\` }).from(tableSchema);
171
-
172
- \t\tif (search) {
173
- const searchConditions: any[] = [];
174
- \t\t\t${searchConditions}
175
- \t\t\tif (searchConditions.length > 0) {
176
- \t\t\t\tquery = query.where(or(...searchConditions)) as any;
177
- \t\t\t\tcountQuery = countQuery.where(or(...searchConditions)) as any;
178
- \t\t\t}
179
- \t\t}
180
-
181
- \t\tif (sort && tableSchema[sort]) {
182
- \t\t\tquery = query.orderBy(order === 'asc' ? asc(tableSchema[sort]) : desc(tableSchema[sort])) as any;
183
- \t\t}
184
-
185
- \t\tconst data = await query.limit(limit).offset(offset).execute();
186
- \t\tconst totalResult = await countQuery.execute();
187
- \t\tconst total = Number(totalResult[0]?.count || 0);
188
- \t\tconst totalPages = Math.ceil(total / limit);
189
-
190
- \t\tconst tableHtml = html\`
191
- \t\t\t<div class="bg-base-100 rounded-xl border border-base-200 overflow-hidden">
192
- \t\t\t\t<div class="overflow-x-auto">
193
- \t\t\t\t\t<table class="table table-sm md:table-md w-full">
194
- \t\t\t\t\t\t<thead>
195
- \t\t\t\t\t\t\t<tr class="border-b border-base-200">
196
- \t\t\t\t\t\t\t\t${headerSelectionCell}
197
- \t\t\t\t\t\t\t\t${headers}
198
- \t\t\t\t\t\t\t\t<th class="w-[100px] text-right">
199
- \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:dots-horizontal" width="16" height="16" class="opacity-30"></iconify-icon>
200
- \t\t\t\t\t\t\t\t</th>
201
- \t\t\t\t\t\t\t</tr>
202
- \t\t\t\t\t\t</thead>
203
- \t\t\t\t\t\t<tbody>
204
- \t\t\t\t\t\t\t\${data.map((row, rowIndex) => html\`
205
- \t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
206
- \t\t\t\t\t\t\t\t${rowSelectionCell}
207
- \t\t\t\t\t\t\t\t${rowCells}
208
- \t\t\t\t\t\t\t\t${actionsCell}
209
- \t\t\t\t\t\t\t</tr>
210
- \t\t\t\t\t\t\t\`)}
211
- \t\t\t\t\t\t\t\${data.length === 0 ? html\`
212
- \t\t\t\t\t\t\t\t<tr>
213
- \t\t\t\t\t\t\t\t\t<td colspan="${columns.length + 2}" class="text-center py-12">
214
- \t\t\t\t\t\t\t\t\t\t<div class="flex flex-col items-center gap-3">
215
- \t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:database-off-outline" width="40" height="40" class="opacity-20"></iconify-icon>
216
- \t\t\t\t\t\t\t\t\t\t\t<p class="text-sm text-base-content/40">No records found.</p>
217
- \t\t\t\t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-primary gap-1">
218
- \t\t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
219
- \t\t\t\t\t\t\t\t\t\t\t\tNew record
220
- \t\t\t\t\t\t\t\t\t\t\t</label>
221
- \t\t\t\t\t\t\t\t\t\t</div>
222
- \t\t\t\t\t\t\t\t\t</td>
223
- \t\t\t\t\t\t\t\t</tr>
224
- \t\t\t\t\t\t\t\` : ''}
225
- \t\t\t\t\t\t</tbody>
226
- \t\t\t\t\t</table>
227
- \t\t\t\t</div>
228
- \t\t\t\t${paginationHtml}
229
- \t\t\t</div>
230
- \t\t\`;
231
-
232
- \t\tconst content = html\`
233
- \t\t\t<div id="main-content">
234
- \t\t\t\t<div class="drawer drawer-end">
235
- \t\t\t\t\t<input id="create-drawer-${table.exportName}" type="checkbox" class="drawer-toggle" />
236
- \t\t\t\t\t<div class="drawer-content">
237
- \t\t\t\t\t\t<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-5 gap-3">
238
- \t\t\t\t\t\t\t<div class="flex items-center gap-2 text-sm">
239
- \t\t\t\t\t\t\t\t<a href="/admin" class="text-base-content/40 hover:text-primary transition-colors">Collections</a>
240
- \t\t\t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="14" height="14" class="opacity-30"></iconify-icon>
241
- \t\t\t\t\t\t\t\t<span class="font-semibold capitalize">${table.tableName}</span>
242
- \t\t\t\t\t\t\t\t<button class="btn btn-ghost btn-xs btn-square opacity-40 hover:opacity-100" onclick="window.location.reload()">
243
- \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:refresh" width="14" height="14"></iconify-icon>
244
- \t\t\t\t\t\t\t\t</button>
245
- \t\t\t\t\t\t\t</div>
246
- \t\t\t\t\t\t\t<div class="flex items-center gap-2 w-full md:w-auto">
247
- \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-primary btn-sm gap-1">
248
- \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
249
- \t\t\t\t\t\t\t\t\tNew record
250
- \t\t\t\t\t\t\t\t</label>
251
- \t\t\t\t\t\t\t</div>
252
- \t\t\t\t\t\t</div>
253
- \t\t\t\t\t\t<div class="mb-4">
254
- \t\t\t\t\t\t\t${searchBarHtml}
255
- \t\t\t\t\t\t</div>
256
- \t\t\t\t\t\t\${tableHtml}
257
- \t\t\t\t\t\t${bulkDeleteUi}
258
- \t\t\t\t\t</div>
259
- \t\t\t\t\t<div class="drawer-side z-50">
260
- \t\t\t\t\t\t<label for="create-drawer-${table.exportName}" aria-label="close sidebar" class="drawer-overlay"></label>
261
- \t\t\t\t\t\t<div class="w-full max-w-md min-h-full bg-base-100 p-6 border-l border-base-200 overflow-y-auto shadow-lg">
262
- \t\t\t\t\t\t\t<div class="flex justify-between items-center mb-5">
263
- \t\t\t\t\t\t\t\t<h3 class="text-base font-semibold">Create ${table.tableName}</h3>
264
- \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-ghost btn-square">
265
- \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:close" width="18" height="18"></iconify-icon>
266
- \t\t\t\t\t\t\t\t</label>
267
- \t\t\t\t\t\t\t</div>
268
- \t\t\t\t\t\t\t<form hx-post="/admin/table/${table.exportName}/create" hx-target="#main-content" hx-swap="outerHTML" class="flex flex-col gap-4">
269
- \t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
270
- \t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
271
- \t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
272
- \t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
273
- \t\t\t\t\t\t\t\t${createInputs}
274
- \t\t\t\t\t\t\t\t<button type="submit" class="btn btn-primary btn-sm mt-2">Create record</button>
275
- \t\t\t\t\t\t\t</form>
276
- \t\t\t\t\t\t</div>
277
- \t\t\t\t\t</div>
278
- \t\t\t\t</div>
279
- \t\t\t</div>
280
- \t\t\`;
281
-
282
- \t\tif (c.req.header('hx-request')) {
283
- \t\t\treturn c.html(content);
284
- \t\t}
285
-
286
- \t\treturn c.html(Layout({
287
- \t\t\ttitle: "${table.tableName} - Admin Dashboard",
288
- \t\t\tchildren: content
289
- \t\t}));
290
- \t});`;
291
- }
1
+ import { DiscoveredTable } from "../../../types";
2
+ import { buildPaginationHtml } from "../common/pagination";
3
+ import { buildSearchBarHtml } from "../common/search-bar";
4
+
5
+ /**
6
+ * Builds the GET route handler code string for /admin/table/:tableName
7
+ * PocketBase-style layout with breadcrumb header, filter bar, and clean table.
8
+ */
9
+ export function buildTableGetRoute(
10
+ table: DiscoveredTable,
11
+ defaultSort: string,
12
+ primaryKey: string,
13
+ hasPrimaryKey: boolean,
14
+ columns: string[],
15
+ searchConditions: string,
16
+ headers: string,
17
+ rowCells: string,
18
+ actionsCell: string,
19
+ createInputs: string,
20
+ ): string {
21
+ const paginationHtml = buildPaginationHtml(
22
+ `/admin/table/${table.exportName}`,
23
+ );
24
+ const searchBarHtml = buildSearchBarHtml(
25
+ `/admin/table/${table.exportName}`,
26
+ "Search term or filter...",
27
+ );
28
+ const headerSelectionCell = hasPrimaryKey
29
+ ? `<th class="w-10"><input id="select-all-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" /></th>`
30
+ : `<th class="w-10"><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></th>`;
31
+ const rowSelectionCell = hasPrimaryKey
32
+ ? `<td><input type="checkbox" class="checkbox checkbox-xs row-select-checkbox" value="\${String((row as any).${primaryKey} ?? '')}" /></td>`
33
+ : `<td><input type="checkbox" class="checkbox checkbox-xs opacity-30" disabled /></td>`;
34
+ const bulkDeleteUi = hasPrimaryKey
35
+ ? `
36
+ \t\t\t\t\t\t<div id="bulk-delete-bar-${table.exportName}" class="fixed bottom-4 left-1/2 -translate-x-1/2 z-40 hidden">
37
+ \t\t\t\t\t\t\t<div class="bg-base-100 border border-base-200 rounded-xl shadow-lg px-3 py-2 flex items-center gap-3">
38
+ \t\t\t\t\t\t\t\t<div class="text-xs text-base-content/70">
39
+ \t\t\t\t\t\t\t\t\t<span id="bulk-selected-count-${table.exportName}" class="font-medium text-base-content">0</span> selected
40
+ \t\t\t\t\t\t\t\t</div>
41
+ \t\t\t\t\t\t\t\t<label class="label cursor-pointer gap-2 py-0 px-1">
42
+ \t\t\t\t\t\t\t\t\t<span class="label-text text-xs">All matching (\${total})</span>
43
+ \t\t\t\t\t\t\t\t\t<input id="bulk-all-matching-${table.exportName}" type="checkbox" class="checkbox checkbox-xs" />
44
+ \t\t\t\t\t\t\t\t</label>
45
+ \t\t\t\t\t\t\t\t<button id="bulk-delete-trigger-${table.exportName}" type="button" class="btn btn-error btn-xs gap-1">
46
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:delete-outline" width="14" height="14"></iconify-icon>
47
+ \t\t\t\t\t\t\t\t\tDelete selected
48
+ \t\t\t\t\t\t\t\t</button>
49
+ \t\t\t\t\t\t\t</div>
50
+ \t\t\t\t\t\t</div>
51
+
52
+ \t\t\t\t\t\t<dialog id="bulk-delete-modal-${table.exportName}" class="modal">
53
+ \t\t\t\t\t\t\t<div class="modal-box max-w-sm p-6 space-y-4">
54
+ \t\t\t\t\t\t\t\t<h3 class="font-semibold text-base">Delete selected rows?</h3>
55
+ \t\t\t\t\t\t\t\t<p class="text-sm text-base-content/70" id="bulk-delete-description-${table.exportName}">This action cannot be undone.</p>
56
+ \t\t\t\t\t\t\t\t<form id="bulk-delete-form-${table.exportName}" hx-post="/admin/table/${table.exportName}/delete-bulk" hx-target="#main-content" hx-swap="outerHTML" class="space-y-3">
57
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="bulkMode" id="bulk-delete-mode-${table.exportName}" value="selected" />
58
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="selectedIds" id="bulk-delete-ids-${table.exportName}" value="" />
59
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
60
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
61
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
62
+ \t\t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
63
+ \t\t\t\t\t\t\t\t\t<div class="flex justify-end gap-2 pt-1">
64
+ \t\t\t\t\t\t\t\t\t\t<button type="button" class="btn btn-ghost btn-sm" onclick="this.closest('dialog')?.close()">Cancel</button>
65
+ \t\t\t\t\t\t\t\t\t\t<button type="submit" class="btn btn-error btn-sm">Delete</button>
66
+ \t\t\t\t\t\t\t\t\t</div>
67
+ \t\t\t\t\t\t\t\t</form>
68
+ \t\t\t\t\t\t\t</div>
69
+ \t\t\t\t\t\t\t<form method="dialog" class="modal-backdrop"><button>close</button></form>
70
+ \t\t\t\t\t\t</dialog>
71
+
72
+ \t\t\t\t\t\t<script>
73
+ \t\t\t\t\t\t\t(() => {
74
+ \t\t\t\t\t\t\t\tconst container = document.querySelector('#main-content');
75
+ \t\t\t\t\t\t\t\tif (!container) return;
76
+
77
+ \t\t\t\t\t\t\t\tconst rowCheckboxes = Array.from(container.querySelectorAll('.row-select-checkbox')).filter((node) => node instanceof HTMLInputElement);
78
+ \t\t\t\t\t\t\t\tif (rowCheckboxes.length === 0) return;
79
+
80
+ \t\t\t\t\t\t\t\tconst selectAll = container.querySelector('#select-all-${table.exportName}');
81
+ \t\t\t\t\t\t\t\tconst bar = container.querySelector('#bulk-delete-bar-${table.exportName}');
82
+ \t\t\t\t\t\t\t\tconst selectedCount = container.querySelector('#bulk-selected-count-${table.exportName}');
83
+ \t\t\t\t\t\t\t\tconst allMatching = container.querySelector('#bulk-all-matching-${table.exportName}');
84
+ \t\t\t\t\t\t\t\tconst trigger = container.querySelector('#bulk-delete-trigger-${table.exportName}');
85
+ \t\t\t\t\t\t\t\tconst modal = container.querySelector('#bulk-delete-modal-${table.exportName}');
86
+ \t\t\t\t\t\t\t\tconst modeInput = container.querySelector('#bulk-delete-mode-${table.exportName}');
87
+ \t\t\t\t\t\t\t\tconst idsInput = container.querySelector('#bulk-delete-ids-${table.exportName}');
88
+ \t\t\t\t\t\t\t\tconst description = container.querySelector('#bulk-delete-description-${table.exportName}');
89
+
90
+ \t\t\t\t\t\t\t\tif (!(selectAll instanceof HTMLInputElement) ||
91
+ \t\t\t\t\t\t\t\t\t!(bar instanceof HTMLElement) ||
92
+ \t\t\t\t\t\t\t\t\t!(selectedCount instanceof HTMLElement) ||
93
+ \t\t\t\t\t\t\t\t\t!(allMatching instanceof HTMLInputElement) ||
94
+ \t\t\t\t\t\t\t\t\t!(trigger instanceof HTMLButtonElement) ||
95
+ \t\t\t\t\t\t\t\t\t!(modal instanceof HTMLDialogElement) ||
96
+ \t\t\t\t\t\t\t\t\t!(modeInput instanceof HTMLInputElement) ||
97
+ \t\t\t\t\t\t\t\t\t!(idsInput instanceof HTMLInputElement) ||
98
+ \t\t\t\t\t\t\t\t\t!(description instanceof HTMLElement)) {
99
+ \t\t\t\t\t\t\t\t\treturn;
100
+ \t\t\t\t\t\t\t\t}
101
+
102
+ \t\t\t\t\t\t\t\tconst totalRows = Number(\${total});
103
+ \t\t\t\t\t\t\t\tconst getSelectedIds = () => rowCheckboxes.filter((checkbox) => checkbox.checked).map((checkbox) => checkbox.value).filter((value) => value.length > 0);
104
+
105
+ \t\t\t\t\t\t\t\tconst updateUi = () => {
106
+ \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
107
+ \t\t\t\t\t\t\t\t\tconst selected = allMatching.checked ? totalRows : ids.length;
108
+ \t\t\t\t\t\t\t\t\tselectedCount.textContent = String(selected);
109
+ \t\t\t\t\t\t\t\t\tbar.classList.toggle('hidden', selected === 0);
110
+ \t\t\t\t\t\t\t\t\tconst checkedCount = ids.length;
111
+ \t\t\t\t\t\t\t\t\tselectAll.checked = checkedCount > 0 && checkedCount === rowCheckboxes.length;
112
+ \t\t\t\t\t\t\t\t\tselectAll.indeterminate = checkedCount > 0 && checkedCount < rowCheckboxes.length;
113
+ \t\t\t\t\t\t\t\t};
114
+
115
+ \t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
116
+ \t\t\t\t\t\t\t\t\tcheckbox.addEventListener('change', () => {
117
+ \t\t\t\t\t\t\t\t\t\tif (allMatching.checked) allMatching.checked = false;
118
+ \t\t\t\t\t\t\t\t\t\tupdateUi();
119
+ \t\t\t\t\t\t\t\t\t});
120
+ \t\t\t\t\t\t\t\t});
121
+
122
+ \t\t\t\t\t\t\t\tselectAll.addEventListener('change', () => {
123
+ \t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
124
+ \t\t\t\t\t\t\t\t\t\tcheckbox.checked = selectAll.checked;
125
+ \t\t\t\t\t\t\t\t\t});
126
+ \t\t\t\t\t\t\t\t\tif (allMatching.checked && !selectAll.checked) allMatching.checked = false;
127
+ \t\t\t\t\t\t\t\t\tupdateUi();
128
+ \t\t\t\t\t\t\t\t});
129
+
130
+ \t\t\t\t\t\t\t\tallMatching.addEventListener('change', () => {
131
+ \t\t\t\t\t\t\t\t\tif (allMatching.checked) {
132
+ \t\t\t\t\t\t\t\t\t\trowCheckboxes.forEach((checkbox) => {
133
+ \t\t\t\t\t\t\t\t\t\t\tcheckbox.checked = true;
134
+ \t\t\t\t\t\t\t\t\t\t});
135
+ \t\t\t\t\t\t\t\t\t}
136
+ \t\t\t\t\t\t\t\t\tupdateUi();
137
+ \t\t\t\t\t\t\t\t});
138
+
139
+ \t\t\t\t\t\t\t\ttrigger.addEventListener('click', () => {
140
+ \t\t\t\t\t\t\t\t\tconst ids = getSelectedIds();
141
+ \t\t\t\t\t\t\t\t\tif (!allMatching.checked && ids.length === 0) return;
142
+ \t\t\t\t\t\t\t\t\tmodeInput.value = allMatching.checked ? 'all-matching' : 'selected';
143
+ \t\t\t\t\t\t\t\t\tidsInput.value = ids.join(',');
144
+ \t\t\t\t\t\t\t\t\tdescription.textContent = allMatching.checked
145
+ \t\t\t\t\t\t\t\t\t\t? 'This will permanently delete all rows matching the current search context.'
146
+ \t\t\t\t\t\t\t\t\t\t: 'This action cannot be undone.';
147
+ \t\t\t\t\t\t\t\t\tmodal.showModal();
148
+ \t\t\t\t\t\t\t\t});
149
+
150
+ \t\t\t\t\t\t\t\tupdateUi();
151
+ \t\t\t\t\t\t\t})();
152
+ \t\t\t\t\t\t</script>
153
+ `
154
+ : "";
155
+
156
+ return `
157
+ \tadminApp.get('/table/${table.exportName}', async (c) => {
158
+ \t\tconst db = drizzle(c.env[options.databaseBinding], { schema });
159
+ \t\tconst page = parseInt(c.req.query('page') || '1');
160
+ \t\tconst limit = 20;
161
+ \t\tconst offset = (page - 1) * limit;
162
+ \t\tconst sort = c.req.query('sort') || '${defaultSort}';
163
+ \t\tconst order = c.req.query('order') || 'desc';
164
+ \t\tconst search = c.req.query('search') || '';
165
+
166
+ \t\tlet tableSchema = (schema as any).${table.exportName};
167
+ \t\tif (!tableSchema) return c.text("Table missing", 404);
168
+
169
+ \t\tlet query = db.select().from(tableSchema);
170
+ \t\tlet countQuery = db.select({ count: sql\`count(*)\` }).from(tableSchema);
171
+
172
+ \t\tif (search) {
173
+ const searchConditions: any[] = [];
174
+ \t\t\t${searchConditions}
175
+ \t\t\tif (searchConditions.length > 0) {
176
+ \t\t\t\tquery = query.where(or(...searchConditions)) as any;
177
+ \t\t\t\tcountQuery = countQuery.where(or(...searchConditions)) as any;
178
+ \t\t\t}
179
+ \t\t}
180
+
181
+ \t\tif (sort && tableSchema[sort]) {
182
+ \t\t\tquery = query.orderBy(order === 'asc' ? asc(tableSchema[sort]) : desc(tableSchema[sort])) as any;
183
+ \t\t}
184
+
185
+ \t\tconst data = await query.limit(limit).offset(offset).execute();
186
+ \t\tconst totalResult = await countQuery.execute();
187
+ \t\tconst total = Number(totalResult[0]?.count || 0);
188
+ \t\tconst totalPages = Math.ceil(total / limit);
189
+
190
+ \t\tconst tableHtml = html\`
191
+ \t\t\t<div class="bg-base-100 rounded-xl border border-base-200 overflow-hidden">
192
+ \t\t\t\t<div class="overflow-x-auto">
193
+ \t\t\t\t\t<table class="table table-sm md:table-md w-full">
194
+ \t\t\t\t\t\t<thead>
195
+ \t\t\t\t\t\t\t<tr class="border-b border-base-200">
196
+ \t\t\t\t\t\t\t\t${headerSelectionCell}
197
+ \t\t\t\t\t\t\t\t${headers}
198
+ \t\t\t\t\t\t\t\t<th class="w-[100px] text-right">
199
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:dots-horizontal" width="16" height="16" class="opacity-30"></iconify-icon>
200
+ \t\t\t\t\t\t\t\t</th>
201
+ \t\t\t\t\t\t\t</tr>
202
+ \t\t\t\t\t\t</thead>
203
+ \t\t\t\t\t\t<tbody>
204
+ \t\t\t\t\t\t\t\${data.map((row, rowIndex) => html\`
205
+ \t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
206
+ \t\t\t\t\t\t\t\t${rowSelectionCell}
207
+ \t\t\t\t\t\t\t\t${rowCells}
208
+ \t\t\t\t\t\t\t\t${actionsCell}
209
+ \t\t\t\t\t\t\t</tr>
210
+ \t\t\t\t\t\t\t\`)}
211
+ \t\t\t\t\t\t\t\${data.length === 0 ? html\`
212
+ \t\t\t\t\t\t\t\t<tr>
213
+ \t\t\t\t\t\t\t\t\t<td colspan="${columns.length + 2}" class="text-center py-12">
214
+ \t\t\t\t\t\t\t\t\t\t<div class="flex flex-col items-center gap-3">
215
+ \t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:database-off-outline" width="40" height="40" class="opacity-20"></iconify-icon>
216
+ \t\t\t\t\t\t\t\t\t\t\t<p class="text-sm text-base-content/40">No records found.</p>
217
+ \t\t\t\t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-primary gap-1">
218
+ \t\t\t\t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
219
+ \t\t\t\t\t\t\t\t\t\t\t\tNew record
220
+ \t\t\t\t\t\t\t\t\t\t\t</label>
221
+ \t\t\t\t\t\t\t\t\t\t</div>
222
+ \t\t\t\t\t\t\t\t\t</td>
223
+ \t\t\t\t\t\t\t\t</tr>
224
+ \t\t\t\t\t\t\t\` : ''}
225
+ \t\t\t\t\t\t</tbody>
226
+ \t\t\t\t\t</table>
227
+ \t\t\t\t</div>
228
+ \t\t\t\t${paginationHtml}
229
+ \t\t\t</div>
230
+ \t\t\`;
231
+
232
+ \t\tconst content = html\`
233
+ \t\t\t<div id="main-content">
234
+ \t\t\t\t<div class="drawer drawer-end">
235
+ \t\t\t\t\t<input id="create-drawer-${table.exportName}" type="checkbox" class="drawer-toggle" />
236
+ \t\t\t\t\t<div class="drawer-content">
237
+ \t\t\t\t\t\t<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-5 gap-3">
238
+ \t\t\t\t\t\t\t<div class="flex items-center gap-2 text-sm">
239
+ \t\t\t\t\t\t\t\t<a href="/admin" class="text-base-content/40 hover:text-primary transition-colors">Collections</a>
240
+ \t\t\t\t\t\t\t\t<iconify-icon icon="mdi:chevron-right" width="14" height="14" class="opacity-30"></iconify-icon>
241
+ \t\t\t\t\t\t\t\t<span class="font-semibold capitalize">${table.tableName}</span>
242
+ \t\t\t\t\t\t\t\t<button class="btn btn-ghost btn-xs btn-square opacity-40 hover:opacity-100" onclick="window.location.reload()">
243
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:refresh" width="14" height="14"></iconify-icon>
244
+ \t\t\t\t\t\t\t\t</button>
245
+ \t\t\t\t\t\t\t</div>
246
+ \t\t\t\t\t\t\t<div class="flex items-center gap-2 w-full md:w-auto">
247
+ \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-primary btn-sm gap-1">
248
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:plus" width="16" height="16"></iconify-icon>
249
+ \t\t\t\t\t\t\t\t\tNew record
250
+ \t\t\t\t\t\t\t\t</label>
251
+ \t\t\t\t\t\t\t</div>
252
+ \t\t\t\t\t\t</div>
253
+ \t\t\t\t\t\t<div class="mb-4">
254
+ \t\t\t\t\t\t\t${searchBarHtml}
255
+ \t\t\t\t\t\t</div>
256
+ \t\t\t\t\t\t\${tableHtml}
257
+ \t\t\t\t\t\t${bulkDeleteUi}
258
+ \t\t\t\t\t</div>
259
+ \t\t\t\t\t<div class="drawer-side z-50">
260
+ \t\t\t\t\t\t<label for="create-drawer-${table.exportName}" aria-label="close sidebar" class="drawer-overlay"></label>
261
+ \t\t\t\t\t\t<div class="w-full max-w-md min-h-full bg-base-100 p-6 border-l border-base-200 overflow-y-auto shadow-lg">
262
+ \t\t\t\t\t\t\t<div class="flex justify-between items-center mb-5">
263
+ \t\t\t\t\t\t\t\t<h3 class="text-base font-semibold">Create ${table.tableName}</h3>
264
+ \t\t\t\t\t\t\t\t<label for="create-drawer-${table.exportName}" class="btn btn-sm btn-ghost btn-square">
265
+ \t\t\t\t\t\t\t\t\t<iconify-icon icon="mdi:close" width="18" height="18"></iconify-icon>
266
+ \t\t\t\t\t\t\t\t</label>
267
+ \t\t\t\t\t\t\t</div>
268
+ \t\t\t\t\t\t\t<form hx-post="/admin/table/${table.exportName}/create" hx-target="#main-content" hx-swap="outerHTML" class="flex flex-col gap-4">
269
+ \t\t\t\t\t\t\t\t<input type="hidden" name="sort" value="\${sort}" />
270
+ \t\t\t\t\t\t\t\t<input type="hidden" name="order" value="\${order}" />
271
+ \t\t\t\t\t\t\t\t<input type="hidden" name="search" value="\${search}" />
272
+ \t\t\t\t\t\t\t\t<input type="hidden" name="page" value="\${page}" />
273
+ \t\t\t\t\t\t\t\t${createInputs}
274
+ \t\t\t\t\t\t\t\t<button type="submit" class="btn btn-primary btn-sm mt-2">Create record</button>
275
+ \t\t\t\t\t\t\t</form>
276
+ \t\t\t\t\t\t</div>
277
+ \t\t\t\t\t</div>
278
+ \t\t\t\t</div>
279
+ \t\t\t</div>
280
+ \t\t\`;
281
+
282
+ \t\tif (c.req.header('hx-request')) {
283
+ \t\t\treturn c.html(content);
284
+ \t\t}
285
+
286
+ \t\treturn c.html(Layout({
287
+ \t\t\ttitle: "${table.tableName} - Admin Dashboard",
288
+ \t\t\tchildren: content
289
+ \t\t}));
290
+ \t});`;
291
+ }