convex-cms 0.0.9-alpha.8 → 0.0.10

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 (136) hide show
  1. package/README.md +27 -0
  2. package/admin/src/components/Header.tsx +1 -1
  3. package/admin/src/components/RouteGuard.tsx +1 -1
  4. package/admin/src/components/UploadDropzone.tsx +1 -1
  5. package/admin/src/components/cmsds/CmsFilterBar.tsx +74 -0
  6. package/admin/src/components/cmsds/CmsInput.tsx +24 -0
  7. package/admin/src/components/cmsds/CmsPagination.tsx +79 -0
  8. package/admin/src/components/cmsds/CmsSelect.tsx +59 -0
  9. package/admin/src/components/cmsds/CmsStatCard.tsx +79 -0
  10. package/admin/src/components/cmsds/CmsStatusBadge.tsx +1 -1
  11. package/admin/src/components/cmsds/index.ts +5 -0
  12. package/admin/src/components/ui/sidebar.tsx +1 -1
  13. package/admin/src/contexts/AuthContext.tsx +1 -1
  14. package/admin/src/contexts/ThemeContext.tsx +85 -17
  15. package/admin/src/embed/components/EmbedHeader.tsx +11 -9
  16. package/admin/src/embed/components/EmbedLayout.tsx +2 -6
  17. package/admin/src/embed/components/EmbedSidebar.tsx +16 -13
  18. package/admin/src/embed/contexts/ApiContext.tsx +1 -1
  19. package/admin/src/embed/index.tsx +3 -2
  20. package/admin/src/embed/types.ts +6 -0
  21. package/admin/src/hooks/usePermissions.ts +1 -1
  22. package/admin/src/index.css +432 -0
  23. package/admin/src/lib/cmsExports.ts +6 -0
  24. package/admin/src/pages/ContentPage.tsx +116 -172
  25. package/admin/src/pages/ContentTypeEntriesPage.tsx +120 -194
  26. package/admin/src/pages/ContentTypesPage.tsx +136 -139
  27. package/admin/src/pages/DashboardPage.tsx +15 -55
  28. package/admin/src/pages/MediaPage.tsx +31 -57
  29. package/admin/src/pages/SettingsPage.tsx +5 -1
  30. package/admin/src/pages/TrashPage.tsx +115 -170
  31. package/admin/src/routes/__root.tsx +1 -1
  32. package/admin-dist/nitro.json +1 -1
  33. package/admin-dist/public/assets/{CmsEmptyState-DTlpzjOI.js → CmsEmptyState-BKeL4DBB.js} +1 -1
  34. package/admin-dist/public/assets/CmsFilterBar-CEpMHd_c.js +1 -0
  35. package/admin-dist/public/assets/{CmsPageHeader-0REGRH4X.js → CmsPageHeader-CIEkTbyH.js} +1 -1
  36. package/admin-dist/public/assets/{CmsStatusBadge-D_n8u8xa.js → CmsStatusBadge-BFMOsfMW.js} +1 -1
  37. package/admin-dist/public/assets/{CmsSurface-BHmvNai4.js → CmsSurface-kqqaFKUI.js} +1 -1
  38. package/admin-dist/public/assets/CmsTable-Db53Exq0.js +1 -0
  39. package/admin-dist/public/assets/ContentEntryEditor-Ct7cHayy.js +4 -0
  40. package/admin-dist/public/assets/TaxonomyFilter-Bm1DI1A7.js +1 -0
  41. package/admin-dist/public/assets/_contentTypeId-BekeCblX.js +1 -0
  42. package/admin-dist/public/assets/{_entryId-jPXz4z9T.js → _entryId-CoZDE0l0.js} +1 -1
  43. package/admin-dist/public/assets/{alert-CG97cMfC.js → alert-CpLdsTGU.js} +1 -1
  44. package/admin-dist/public/assets/{badge-C6qt24oj.js → badge-BQAotc5B.js} +1 -1
  45. package/admin-dist/public/assets/{circle-check-big-PltpxuB1.js → circle-check-big-BF3Y5nES.js} +1 -1
  46. package/admin-dist/public/assets/{command-CJ8i86fd.js → command-lEq6f_Ee.js} +1 -1
  47. package/admin-dist/public/assets/content-DH6k0dN6.js +1 -0
  48. package/admin-dist/public/assets/content-types-DHr9tc2V.js +1 -0
  49. package/admin-dist/public/assets/index-Cf0lbl0G.js +1 -0
  50. package/admin-dist/public/assets/index-D-4wFfgU.css +1 -0
  51. package/admin-dist/public/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  52. package/admin-dist/public/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  53. package/admin-dist/public/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  54. package/admin-dist/public/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  55. package/admin-dist/public/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  56. package/admin-dist/public/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  57. package/admin-dist/public/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  58. package/admin-dist/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  59. package/admin-dist/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  60. package/admin-dist/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  61. package/admin-dist/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  62. package/admin-dist/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  63. package/admin-dist/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  64. package/admin-dist/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  65. package/admin-dist/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  66. package/admin-dist/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  67. package/admin-dist/public/assets/main-B-6700eG.js +137 -0
  68. package/admin-dist/public/assets/media-DY5zD52L.js +1 -0
  69. package/admin-dist/public/assets/{new._contentTypeId-qsvo01mH.js → new._contentTypeId-Dq_NqTQV.js} +1 -1
  70. package/admin-dist/public/assets/{pencil-gAL0R34f.js → pencil-CI_KfxSx.js} +1 -1
  71. package/admin-dist/public/assets/refresh-cw-BrXg9a2r.js +1 -0
  72. package/admin-dist/public/assets/rotate-ccw-PwzxdPxd.js +1 -0
  73. package/admin-dist/public/assets/{scroll-area-CJBhf9pf.js → scroll-area-DX_nZYp8.js} +1 -1
  74. package/admin-dist/public/assets/{search-WXp6KxDJ.js → search-DlwBH4C5.js} +1 -1
  75. package/admin-dist/public/assets/settings-2mx3_ORG.js +1 -0
  76. package/admin-dist/public/assets/{switch-Ck9ecqEX.js → switch-CjPi4DKH.js} +1 -1
  77. package/admin-dist/public/assets/{tabs-vQYu8rjC.js → tabs-B5X37GEM.js} +1 -1
  78. package/admin-dist/public/assets/tanstack-adapter-KSm-nO5L.js +1 -0
  79. package/admin-dist/public/assets/{taxonomies-DvILUNvr.js → taxonomies-CHjJKNlR.js} +1 -1
  80. package/admin-dist/public/assets/trash-Cle-tcqq.js +1 -0
  81. package/admin-dist/public/assets/{useBreadcrumbLabel-tlSh7dtO.js → useBreadcrumbLabel-yZQG_N_3.js} +1 -1
  82. package/admin-dist/public/assets/{usePermissions-BTGdTOJS.js → usePermissions-D6vsoaJf.js} +1 -1
  83. package/admin-dist/server/_libs/convex-helpers.mjs +1077 -2
  84. package/admin-dist/server/_libs/convex.mjs +222 -13
  85. package/admin-dist/server/_libs/lucide-react.mjs +57 -51
  86. package/admin-dist/server/_ssr/{CmsEmptyState-CB6e53i5.mjs → CmsEmptyState-DzzuQG0S.mjs} +1 -1
  87. package/admin-dist/server/_ssr/CmsFilterBar-C5XADS12.mjs +81 -0
  88. package/admin-dist/server/_ssr/{CmsPageHeader-COUHuECp.mjs → CmsPageHeader-DZ6h7smh.mjs} +1 -1
  89. package/admin-dist/server/_ssr/{CmsStatusBadge-kMTL6koE.mjs → CmsStatusBadge-D-YFSAa1.mjs} +3 -3
  90. package/admin-dist/server/_ssr/{CmsSurface-D1HDYjRg.mjs → CmsSurface-Cv51NBLZ.mjs} +1 -1
  91. package/admin-dist/server/_ssr/CmsTable-DG88C5nO.mjs +189 -0
  92. package/admin-dist/server/_ssr/{ContentEntryEditor-Bq8FR_uK.mjs → ContentEntryEditor-CRjwXB17.mjs} +10 -10
  93. package/admin-dist/server/_ssr/{TaxonomyFilter-bm_p4ADg.mjs → TaxonomyFilter-xGwcgtjr.mjs} +3 -3
  94. package/admin-dist/server/_ssr/{_contentTypeId-B7obLmi_.mjs → _contentTypeId-DRCfeKkm.mjs} +53 -12
  95. package/admin-dist/server/_ssr/{_entryId-B4zhQqFg.mjs → _entryId-DULm2TDy.mjs} +11 -11
  96. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-iX3K33p1.mjs +4 -0
  97. package/admin-dist/server/_ssr/{badge-NOEC9bkk.mjs → badge-CbjIvhb6.mjs} +1 -1
  98. package/admin-dist/server/_ssr/{command-h4-OYNBo.mjs → command-xB2uiYps.mjs} +2 -2
  99. package/admin-dist/server/_ssr/{content-CShtLuhK.mjs → content-BfLBaJCZ.mjs} +108 -138
  100. package/admin-dist/server/_ssr/{content-types-PeyRyfbc.mjs → content-types-DZbF6O2q.mjs} +130 -119
  101. package/admin-dist/server/_ssr/{index-CplFXpGg.mjs → index-Cfe8sZv5.mjs} +65 -39
  102. package/admin-dist/server/_ssr/index.mjs +2 -2
  103. package/admin-dist/server/_ssr/{media-QAkNdX54.mjs → media-Bds2AnPC.mjs} +36 -56
  104. package/admin-dist/server/_ssr/{new._contentTypeId-DEJyMphJ.mjs → new._contentTypeId-DGvz_tlW.mjs} +10 -10
  105. package/admin-dist/server/_ssr/{router-CQXMuGMF.mjs → router-DxF7GBcO.mjs} +8804 -4995
  106. package/admin-dist/server/_ssr/{scroll-area-B7zoNyWB.mjs → scroll-area-DLDlXI07.mjs} +1 -1
  107. package/admin-dist/server/_ssr/{settings-CNaqVa4D.mjs → settings-BbaiS6z9.mjs} +13 -10
  108. package/admin-dist/server/_ssr/{switch-BKZhvryc.mjs → switch-Bl89Pfxu.mjs} +1 -1
  109. package/admin-dist/server/_ssr/{tabs-DtIIQxiD.mjs → tabs-QkbR0iir.mjs} +3 -3
  110. package/admin-dist/server/_ssr/{tanstack-adapter-CLavdbUY.mjs → tanstack-adapter-CKknPtcU.mjs} +19 -1
  111. package/admin-dist/server/_ssr/{taxonomies-vIZYICzr.mjs → taxonomies-S_Ontd0z.mjs} +9 -9
  112. package/admin-dist/server/_ssr/{trash-7yGR4-dF.mjs → trash-BzAIsbbN.mjs} +109 -132
  113. package/admin-dist/server/_ssr/{useBreadcrumbLabel-DR5FaAMf.mjs → useBreadcrumbLabel-BjiR1fM_.mjs} +1 -1
  114. package/admin-dist/server/_ssr/{usePermissions-DKkpETj_.mjs → usePermissions-CDHN95Nz.mjs} +1 -1
  115. package/admin-dist/server/index.mjs +284 -165
  116. package/package.json +3 -2
  117. package/admin/src/styles/globals.css +0 -104
  118. package/admin/src/styles/tailwind-config.css +0 -99
  119. package/admin/src/styles/theme.css +0 -261
  120. package/admin-dist/public/assets/CmsToolbar-CY6GV2L8.js +0 -1
  121. package/admin-dist/public/assets/ContentEntryEditor-CRgcRkk5.js +0 -4
  122. package/admin-dist/public/assets/TaxonomyFilter-Ohv5Jg9c.js +0 -1
  123. package/admin-dist/public/assets/_contentTypeId-C_vJq22X.js +0 -1
  124. package/admin-dist/public/assets/content-pKaIL2ru.js +0 -1
  125. package/admin-dist/public/assets/content-types-Bl_8I1Re.js +0 -1
  126. package/admin-dist/public/assets/globals-CoCRjt0K.css +0 -1
  127. package/admin-dist/public/assets/index-CtHq_P5q.js +0 -1
  128. package/admin-dist/public/assets/main-CA-4LyFT.js +0 -107
  129. package/admin-dist/public/assets/media-Bl1tBbJQ.js +0 -1
  130. package/admin-dist/public/assets/refresh-cw-sdVUGJNs.js +0 -1
  131. package/admin-dist/public/assets/rotate-ccw-6OcXCcxb.js +0 -1
  132. package/admin-dist/public/assets/settings-D8crrFCn.js +0 -1
  133. package/admin-dist/public/assets/tanstack-adapter-BRt2CUCw.js +0 -1
  134. package/admin-dist/public/assets/trash-YyYaC3L9.js +0 -1
  135. package/admin-dist/server/_ssr/CmsToolbar-NB014hsd.mjs +0 -48
  136. package/admin-dist/server/_ssr/_tanstack-start-manifest_v-DndoqCo7.mjs +0 -4
@@ -8,28 +8,19 @@
8
8
  import { useState, useMemo, useEffect, useCallback } from "react";
9
9
  import { useQuery, useMutation } from "convex/react";
10
10
  import { usePermissions } from "~/hooks";
11
- import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
12
- import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
13
- import { CmsButton } from "~/components/cmsds/CmsButton";
14
- import { CmsStatusBadge, type ContentStatus } from "~/components/cmsds/CmsStatusBadge";
15
- import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
16
- import { CmsConfirmDialog } from "~/components/cmsds/CmsDialog";
17
- import { Input } from "~/components/ui/input";
18
11
  import {
19
- Select,
20
- SelectContent,
21
- SelectItem,
22
- SelectTrigger,
23
- SelectValue,
24
- } from "~/components/ui/select";
25
- import {
26
- Search,
27
- Plus,
28
- FileText,
29
- ChevronUp,
30
- ChevronDown,
31
- ArrowUpDown,
32
- } from "lucide-react";
12
+ CmsPageHeader,
13
+ CmsButton,
14
+ CmsStatusBadge,
15
+ type ContentStatus,
16
+ CmsEmptyState,
17
+ CmsConfirmDialog,
18
+ CmsFilterBar,
19
+ CmsTable,
20
+ type CmsTableColumn,
21
+ CmsPagination,
22
+ } from "~/components/cmsds";
23
+ import { Plus, FileText } from "lucide-react";
33
24
  import type { AdminNavigation } from "~/lib/navigation";
34
25
  import type { CmsAdminApi } from "~/embed/contexts/ApiContext";
35
26
 
@@ -143,7 +134,8 @@ export function ContentTypeEntriesPage({
143
134
  });
144
135
  };
145
136
 
146
- const handleSort = (field: SortField) => {
137
+ const handleSort = (columnKey: string) => {
138
+ const field = columnKey as SortField;
147
139
  if (sortField === field) {
148
140
  setSortDirection(sortDirection === "asc" ? "desc" : "asc");
149
141
  } else {
@@ -152,17 +144,6 @@ export function ContentTypeEntriesPage({
152
144
  }
153
145
  };
154
146
 
155
- const getSortIcon = (field: SortField) => {
156
- if (sortField !== field) {
157
- return <ArrowUpDown className="size-3.5 text-muted-foreground/50" />;
158
- }
159
- return sortDirection === "asc" ? (
160
- <ChevronUp className="size-3.5" />
161
- ) : (
162
- <ChevronDown className="size-3.5" />
163
- );
164
- };
165
-
166
147
  const handleDeleteClick = useCallback(
167
148
  (entry: { _id: string; data: Record<string, unknown> }) => {
168
149
  const title = getEntryTitle(entry);
@@ -212,6 +193,71 @@ export function ContentTypeEntriesPage({
212
193
  setCurrentPage(0);
213
194
  }, []);
214
195
 
196
+ type Entry = (typeof paginatedEntries)[number];
197
+
198
+ const entryColumns: CmsTableColumn<Entry>[] = useMemo(
199
+ () => [
200
+ {
201
+ key: "title",
202
+ header: "Title",
203
+ sortable: true,
204
+ cell: (entry) => (
205
+ <button
206
+ type="button"
207
+ onClick={() => navigation.navigateToEntry(entry._id)}
208
+ className="block text-left"
209
+ >
210
+ <span className="font-medium text-foreground hover:text-primary hover:underline">
211
+ {getEntryTitle(entry)}
212
+ </span>
213
+ <span className="block text-xs text-muted-foreground">{entry.slug}</span>
214
+ </button>
215
+ ),
216
+ },
217
+ {
218
+ key: "status",
219
+ header: "Status",
220
+ sortable: true,
221
+ cell: (entry) => <CmsStatusBadge status={entry.status as ContentStatus} />,
222
+ },
223
+ {
224
+ key: "updatedAt",
225
+ header: "Updated",
226
+ sortable: true,
227
+ cell: (entry) => (
228
+ <span className="text-sm text-muted-foreground">
229
+ {formatDate(entry.lastPublishedAt ?? entry._creationTime)}
230
+ </span>
231
+ ),
232
+ },
233
+ {
234
+ key: "actions",
235
+ header: "Actions",
236
+ cell: (entry) => (
237
+ <div className="flex items-center gap-2">
238
+ <CmsButton
239
+ variant="outline"
240
+ size="sm"
241
+ onClick={() => navigation.navigateToEntry(entry._id)}
242
+ >
243
+ {canUpdate("contentEntries") ? "Edit" : "View"}
244
+ </CmsButton>
245
+ {canDelete("contentEntries") && (
246
+ <CmsButton
247
+ variant="danger"
248
+ size="sm"
249
+ onClick={() => handleDeleteClick(entry)}
250
+ >
251
+ Delete
252
+ </CmsButton>
253
+ )}
254
+ </div>
255
+ ),
256
+ },
257
+ ],
258
+ [navigation, getEntryTitle, formatDate, canUpdate, canDelete, handleDeleteClick]
259
+ );
260
+
215
261
  if (contentType === undefined || entriesResult === undefined) {
216
262
  return (
217
263
  <div className="space-y-6 p-6">
@@ -256,44 +302,32 @@ export function ContentTypeEntriesPage({
256
302
  }
257
303
  />
258
304
 
259
- <CmsToolbar
260
- left={
261
- <div className="flex items-center gap-3">
262
- <div className="relative">
263
- <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
264
- <Input
265
- type="search"
266
- placeholder="Search entries..."
267
- value={searchQuery}
268
- onChange={(e) => setSearchQuery(e.target.value)}
269
- className="w-64 pl-9"
270
- />
271
- </div>
272
- <Select
273
- value={selectedStatus}
274
- onValueChange={(value) => {
275
- setSelectedStatus(value as ContentStatus | "all");
276
- setCurrentPage(0);
277
- }}
278
- >
279
- <SelectTrigger className="w-36">
280
- <SelectValue placeholder="All Statuses" />
281
- </SelectTrigger>
282
- <SelectContent>
283
- <SelectItem value="all">All Statuses</SelectItem>
284
- <SelectItem value="draft">Draft</SelectItem>
285
- <SelectItem value="published">Published</SelectItem>
286
- <SelectItem value="scheduled">Scheduled</SelectItem>
287
- <SelectItem value="archived">Archived</SelectItem>
288
- </SelectContent>
289
- </Select>
290
- </div>
291
- }
292
- right={
293
- <span className="text-sm text-muted-foreground">
294
- {sortedEntries.length} {sortedEntries.length === 1 ? "entry" : "entries"}
295
- </span>
296
- }
305
+ <CmsFilterBar
306
+ search={{
307
+ value: searchQuery,
308
+ onChange: setSearchQuery,
309
+ placeholder: "Search entries...",
310
+ className: "w-64",
311
+ }}
312
+ filters={[
313
+ {
314
+ key: "status",
315
+ value: selectedStatus,
316
+ onChange: (v) => {
317
+ setSelectedStatus(v as ContentStatus | "all");
318
+ setCurrentPage(0);
319
+ },
320
+ options: [
321
+ { value: "all", label: "All Statuses" },
322
+ { value: "draft", label: "Draft" },
323
+ { value: "published", label: "Published" },
324
+ { value: "scheduled", label: "Scheduled" },
325
+ { value: "archived", label: "Archived" },
326
+ ],
327
+ className: "w-36",
328
+ },
329
+ ]}
330
+ onClearFilters={hasFilters ? clearFilters : undefined}
297
331
  />
298
332
 
299
333
  {sortedEntries.length === 0 ? (
@@ -313,129 +347,21 @@ export function ContentTypeEntriesPage({
313
347
  />
314
348
  ) : (
315
349
  <>
316
- <div className="rounded-lg border bg-card">
317
- <table className="w-full">
318
- <thead>
319
- <tr className="border-b">
320
- <th className="p-3 text-left">
321
- <button
322
- className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
323
- onClick={() => handleSort("title")}
324
- >
325
- Title
326
- {getSortIcon("title")}
327
- </button>
328
- </th>
329
- <th className="p-3 text-left">
330
- <button
331
- className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
332
- onClick={() => handleSort("status")}
333
- >
334
- Status
335
- {getSortIcon("status")}
336
- </button>
337
- </th>
338
- <th className="p-3 text-left">
339
- <button
340
- className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
341
- onClick={() => handleSort("updatedAt")}
342
- >
343
- Updated
344
- {getSortIcon("updatedAt")}
345
- </button>
346
- </th>
347
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
348
- Actions
349
- </th>
350
- </tr>
351
- </thead>
352
- <tbody>
353
- {paginatedEntries.map((entry) => (
354
- <tr
355
- key={entry._id}
356
- className="border-b last:border-0 transition-colors hover:bg-muted/50"
357
- >
358
- <td className="p-3">
359
- <button
360
- type="button"
361
- onClick={() => navigation.navigateToEntry(entry._id)}
362
- className="text-left font-medium text-foreground hover:text-primary hover:underline"
363
- >
364
- {getEntryTitle(entry)}
365
- </button>
366
- <p className="text-xs text-muted-foreground">{entry.slug}</p>
367
- </td>
368
- <td className="p-3">
369
- <CmsStatusBadge status={entry.status as ContentStatus} />
370
- </td>
371
- <td className="p-3 text-sm text-muted-foreground">
372
- {formatDate(entry.lastPublishedAt ?? entry._creationTime)}
373
- </td>
374
- <td className="p-3">
375
- <div className="flex items-center gap-2">
376
- <CmsButton
377
- variant="outline"
378
- size="sm"
379
- onClick={() => navigation.navigateToEntry(entry._id)}
380
- >
381
- {canUpdate("contentEntries") ? "Edit" : "View"}
382
- </CmsButton>
383
- {canDelete("contentEntries") && (
384
- <CmsButton
385
- variant="danger"
386
- size="sm"
387
- onClick={() => handleDeleteClick(entry)}
388
- >
389
- Delete
390
- </CmsButton>
391
- )}
392
- </div>
393
- </td>
394
- </tr>
395
- ))}
396
- </tbody>
397
- </table>
398
- </div>
399
-
400
- {totalPages > 1 && (
401
- <div className="flex items-center justify-center gap-2">
402
- <CmsButton
403
- variant="outline"
404
- size="sm"
405
- onClick={() => setCurrentPage(0)}
406
- disabled={currentPage === 0}
407
- >
408
- First
409
- </CmsButton>
410
- <CmsButton
411
- variant="outline"
412
- size="sm"
413
- onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
414
- disabled={currentPage === 0}
415
- >
416
- Previous
417
- </CmsButton>
418
- <span className="px-3 text-sm text-muted-foreground">
419
- Page {currentPage + 1} of {totalPages}
420
- </span>
421
- <CmsButton
422
- variant="outline"
423
- size="sm"
424
- onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
425
- disabled={currentPage >= totalPages - 1}
426
- >
427
- Next
428
- </CmsButton>
429
- <CmsButton
430
- variant="outline"
431
- size="sm"
432
- onClick={() => setCurrentPage(totalPages - 1)}
433
- disabled={currentPage >= totalPages - 1}
434
- >
435
- Last
436
- </CmsButton>
437
- </div>
438
- )}
350
+ <CmsTable
351
+ columns={entryColumns}
352
+ data={paginatedEntries}
353
+ getRowId={(e) => e._id}
354
+ sortColumn={sortField}
355
+ sortDirection={sortDirection}
356
+ onSort={handleSort}
357
+ emptyMessage="No entries found"
358
+ />
359
+
360
+ <CmsPagination
361
+ currentPage={currentPage + 1}
362
+ totalPages={totalPages}
363
+ onPageChange={(page) => setCurrentPage(page - 1)}
364
+ />
439
365
  </>
440
366
  )}
441
367
 
@@ -27,16 +27,18 @@ interface ContentTypeWithCount {
27
27
  _creationTime: number;
28
28
  source?: "code" | "database";
29
29
  }
30
- import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
31
- import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
32
- import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
33
- import { CmsButton } from "~/components/cmsds/CmsButton";
34
- import { Input } from "~/components/ui/input";
30
+ import {
31
+ CmsPageHeader,
32
+ CmsEmptyState,
33
+ CmsButton,
34
+ CmsFilterBar,
35
+ CmsTable,
36
+ type CmsTableColumn,
37
+ } from "~/components/cmsds";
35
38
  import { Checkbox } from "~/components/ui/checkbox";
36
39
  import { Badge } from "~/components/ui/badge";
37
40
  import { cn } from "~/lib/cn";
38
41
  import {
39
- Search,
40
42
  Grid3X3,
41
43
  List,
42
44
  Plus,
@@ -150,6 +152,120 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
150
152
  }
151
153
  };
152
154
 
155
+ const contentTypeColumns: CmsTableColumn<ContentTypeWithCount>[] = useMemo(
156
+ () => [
157
+ {
158
+ key: "name",
159
+ header: "Name",
160
+ cell: (contentType) => (
161
+ <>
162
+ <p className="font-medium text-foreground">{contentType.displayName}</p>
163
+ <p className="text-xs text-muted-foreground">{contentType.name}</p>
164
+ </>
165
+ ),
166
+ },
167
+ {
168
+ key: "fields",
169
+ header: "Fields",
170
+ cell: (contentType) => (
171
+ <span className="text-sm text-muted-foreground">
172
+ {contentType.fields.length}
173
+ </span>
174
+ ),
175
+ },
176
+ {
177
+ key: "entries",
178
+ header: "Entries",
179
+ cell: (contentType) => (
180
+ <span className="text-sm text-muted-foreground">
181
+ {contentType.entryCount ?? 0}
182
+ </span>
183
+ ),
184
+ },
185
+ {
186
+ key: "status",
187
+ header: "Status",
188
+ cell: (contentType) => (
189
+ <div className="flex items-center gap-1.5">
190
+ {contentType.source === "code" && (
191
+ <Badge
192
+ variant="secondary"
193
+ className="border-violet-200 bg-violet-50 text-xs font-normal text-violet-700"
194
+ title="Managed by code"
195
+ >
196
+ <Code2 className="mr-1 size-3" />
197
+ Code
198
+ </Badge>
199
+ )}
200
+ <Badge
201
+ variant={contentType.isActive ? "default" : "secondary"}
202
+ className={cn(
203
+ "text-xs font-normal",
204
+ contentType.isActive &&
205
+ "border-diff-added-border bg-diff-added-bg text-diff-added-foreground",
206
+ )}
207
+ >
208
+ {contentType.isActive ? "Active" : "Inactive"}
209
+ </Badge>
210
+ {contentType.singleton && (
211
+ <Badge
212
+ variant="secondary"
213
+ className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
214
+ >
215
+ Singleton
216
+ </Badge>
217
+ )}
218
+ </div>
219
+ ),
220
+ },
221
+ {
222
+ key: "updated",
223
+ header: "Last Updated",
224
+ cell: (contentType) => (
225
+ <span className="text-sm text-muted-foreground">
226
+ {formatDate(contentType._creationTime)}
227
+ </span>
228
+ ),
229
+ },
230
+ {
231
+ key: "actions",
232
+ header: "Actions",
233
+ cell: (contentType) => (
234
+ <div className="flex items-center gap-2">
235
+ {contentType.source === "code" ? (
236
+ <CmsButton
237
+ variant="outline"
238
+ size="sm"
239
+ onClick={() => setEditingContentType(contentType)}
240
+ title="View content type (managed by code)"
241
+ >
242
+ <Eye className="size-3.5" />
243
+ View
244
+ </CmsButton>
245
+ ) : (
246
+ <CmsButton
247
+ variant="outline"
248
+ size="sm"
249
+ onClick={() => setEditingContentType(contentType)}
250
+ >
251
+ <Pencil className="size-3.5" />
252
+ Edit
253
+ </CmsButton>
254
+ )}
255
+ <CmsButton
256
+ variant="outline"
257
+ size="sm"
258
+ onClick={() => navigation.navigateToContentType(contentType._id)}
259
+ >
260
+ View Entries
261
+ </CmsButton>
262
+ </div>
263
+ ),
264
+ },
265
+ ],
266
+ [navigation, formatDate, setEditingContentType]
267
+ );
268
+
153
269
  return (
154
270
  <div className="space-y-6 p-6">
155
271
  <CmsPageHeader
@@ -157,19 +273,15 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
157
273
  description="Define the structure of your content with custom fields and validation rules."
158
274
  />
159
275
 
160
- <CmsToolbar
161
- left={
276
+ <CmsFilterBar
277
+ search={{
278
+ value: searchQuery,
279
+ onChange: setSearchQuery,
280
+ placeholder: "Search content types...",
281
+ className: "w-64",
282
+ }}
283
+ actions={
162
284
  <div className="flex items-center gap-3">
163
- <div className="relative">
164
- <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
165
- <Input
166
- type="search"
167
- placeholder="Search content types..."
168
- value={searchQuery}
169
- onChange={(e) => setSearchQuery(e.target.value)}
170
- className="w-64 pl-9"
171
- />
172
- </div>
173
285
  <label className="flex cursor-pointer items-center gap-2 text-sm">
174
286
  <Checkbox
175
287
  checked={showActiveOnly}
@@ -179,10 +291,6 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
179
291
  />
180
292
  Active only
181
293
  </label>
182
- </div>
183
- }
184
- right={
185
- <div className="flex items-center gap-2">
186
294
  <div className="flex rounded-md border bg-muted/30">
187
295
  <button
188
296
  className={cn(
@@ -361,123 +469,12 @@ export function ContentTypesPage({ api, navigation }: ContentTypesPageProps) {
361
469
  ))}
362
470
  </div>
363
471
  ) : (
364
- <div className="rounded-lg border bg-card">
365
- <table className="w-full">
366
- <thead>
367
- <tr className="border-b">
368
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
369
- Name
370
- </th>
371
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
372
- Fields
373
- </th>
374
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
375
- Entries
376
- </th>
377
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
378
- Status
379
- </th>
380
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
381
- Last Updated
382
- </th>
383
- <th className="p-3 text-left text-sm font-medium text-muted-foreground">
384
- Actions
385
- </th>
386
- </tr>
387
- </thead>
388
- <tbody>
389
- {filteredContentTypes.map((contentType) => (
390
- <tr
391
- key={contentType._id}
392
- className="border-b last:border-0 transition-colors hover:bg-muted/50"
393
- >
394
- <td className="p-3">
395
- <p className="font-medium text-foreground">
396
- {contentType.displayName}
397
- </p>
398
- <p className="text-xs text-muted-foreground">
399
- {contentType.name}
400
- </p>
401
- </td>
402
- <td className="p-3 text-sm text-muted-foreground">
403
- {contentType.fields.length}
404
- </td>
405
- <td className="p-3 text-sm text-muted-foreground">
406
- {contentType.entryCount ?? 0}
407
- </td>
408
- <td className="p-3">
409
- <div className="flex items-center gap-1.5">
410
- {contentType.source === "code" && (
411
- <Badge
412
- variant="secondary"
413
- className="border-violet-200 bg-violet-50 text-xs font-normal text-violet-700"
414
- title="Managed by code"
415
- >
416
- <Code2 className="mr-1 size-3" />
417
- Code
418
- </Badge>
419
- )}
420
- <Badge
421
- variant={contentType.isActive ? "default" : "secondary"}
422
- className={cn(
423
- "text-xs font-normal",
424
- contentType.isActive &&
425
- "border-diff-added-border bg-diff-added-bg text-diff-added-foreground",
426
- )}
427
- >
428
- {contentType.isActive ? "Active" : "Inactive"}
429
- </Badge>
430
- {contentType.singleton && (
431
- <Badge
432
- variant="secondary"
433
- className="border-diff-modified-border bg-diff-modified-bg text-xs font-normal text-diff-modified-foreground"
434
- >
435
- Singleton
436
- </Badge>
437
- )}
438
- </div>
439
- </td>
440
- <td className="p-3 text-sm text-muted-foreground">
441
- {formatDate(contentType._creationTime)}
442
- </td>
443
- <td className="p-3">
444
- <div className="flex items-center gap-2">
445
- {contentType.source === "code" ? (
446
- <CmsButton
447
- variant="outline"
448
- size="sm"
449
- onClick={() => setEditingContentType(contentType)}
450
- title="View content type (managed by code)"
451
- >
452
- <Eye className="size-3.5" />
453
- View
454
- </CmsButton>
455
- ) : (
456
- <CmsButton
457
- variant="outline"
458
- size="sm"
459
- onClick={() => setEditingContentType(contentType)}
460
- >
461
- <Pencil className="size-3.5" />
462
- Edit
463
- </CmsButton>
464
- )}
465
- <CmsButton
466
- variant="outline"
467
- size="sm"
468
- onClick={() =>
469
- navigation.navigateToContentType(contentType._id)
470
- }
471
- >
472
- View Entries
473
- </CmsButton>
474
- </div>
475
- </td>
476
- </tr>
477
- ))}
478
- </tbody>
479
- </table>
480
- </div>
472
+ <CmsTable
473
+ columns={contentTypeColumns}
474
+ data={filteredContentTypes}
475
+ getRowId={(ct) => ct._id}
476
+ emptyMessage="No content types found"
477
+ />
481
478
  )}
482
479
 
483
480
  {!isLoading && filteredContentTypes.length > 0 && (