convex-cms 0.0.9-alpha.8 → 0.0.9-alpha.9
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.
- package/README.md +27 -0
- package/admin/src/components/cmsds/CmsFilterBar.tsx +74 -0
- package/admin/src/components/cmsds/CmsInput.tsx +24 -0
- package/admin/src/components/cmsds/CmsPagination.tsx +79 -0
- package/admin/src/components/cmsds/CmsSelect.tsx +59 -0
- package/admin/src/components/cmsds/CmsStatCard.tsx +79 -0
- package/admin/src/components/cmsds/CmsStatusBadge.tsx +1 -1
- package/admin/src/components/cmsds/index.ts +5 -0
- package/admin/src/contexts/ThemeContext.tsx +85 -17
- package/admin/src/embed/components/EmbedHeader.tsx +11 -9
- package/admin/src/embed/components/EmbedLayout.tsx +2 -6
- package/admin/src/embed/components/EmbedSidebar.tsx +7 -4
- package/admin/src/embed/index.tsx +3 -2
- package/admin/src/embed/types.ts +6 -0
- package/admin/src/pages/ContentPage.tsx +116 -172
- package/admin/src/pages/ContentTypeEntriesPage.tsx +120 -194
- package/admin/src/pages/ContentTypesPage.tsx +136 -139
- package/admin/src/pages/DashboardPage.tsx +13 -52
- package/admin/src/pages/MediaPage.tsx +31 -57
- package/admin/src/pages/SettingsPage.tsx +5 -1
- package/admin/src/pages/TrashPage.tsx +115 -170
- package/admin/src/styles/globals.css +10 -32
- package/admin/src/styles/tailwind-config.css +12 -0
- package/admin/src/styles/theme.css +229 -106
- package/package.json +1 -1
|
@@ -5,35 +5,27 @@
|
|
|
5
5
|
* Used by both CLI routes and embed pages.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useCallback, useEffect } from "react";
|
|
8
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
9
9
|
import { useQuery } from "convex/react";
|
|
10
10
|
import { usePermissions } from "~/hooks";
|
|
11
11
|
import { BulkActionBar } from "~/components/BulkActionBar";
|
|
12
|
-
import { CmsPageHeader } from "~/components/cmsds/CmsPageHeader";
|
|
13
|
-
import { CmsToolbar } from "~/components/cmsds/CmsToolbar";
|
|
14
|
-
import { CmsEmptyState } from "~/components/cmsds/CmsEmptyState";
|
|
15
12
|
import {
|
|
13
|
+
CmsPageHeader,
|
|
14
|
+
CmsEmptyState,
|
|
16
15
|
CmsStatusBadge,
|
|
17
16
|
type ContentStatus,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
SelectContent,
|
|
24
|
-
SelectItem,
|
|
25
|
-
SelectTrigger,
|
|
26
|
-
SelectValue,
|
|
27
|
-
} from "~/components/ui/select";
|
|
17
|
+
CmsButton,
|
|
18
|
+
CmsFilterBar,
|
|
19
|
+
CmsTable,
|
|
20
|
+
type CmsTableColumn,
|
|
21
|
+
} from "~/components/cmsds";
|
|
28
22
|
import {
|
|
29
23
|
DropdownMenu,
|
|
30
24
|
DropdownMenuContent,
|
|
31
25
|
DropdownMenuItem,
|
|
32
26
|
DropdownMenuTrigger,
|
|
33
27
|
} from "~/components/ui/dropdown-menu";
|
|
34
|
-
import {
|
|
35
|
-
import { cn } from "~/lib/cn";
|
|
36
|
-
import { Plus, Search, FileText, ChevronDown } from "lucide-react";
|
|
28
|
+
import { Plus, FileText, ChevronDown } from "lucide-react";
|
|
37
29
|
import type { AdminNavigation } from "~/lib/navigation";
|
|
38
30
|
import { CmsAdminApi } from "~/embed/contexts/ApiContext";
|
|
39
31
|
|
|
@@ -114,30 +106,72 @@ export function ContentPage({ api, navigation }: ContentPageProps) {
|
|
|
114
106
|
});
|
|
115
107
|
};
|
|
116
108
|
|
|
117
|
-
const handleSelectItem = useCallback((id: string, selected: boolean) => {
|
|
118
|
-
setSelectedIds((prev) => {
|
|
119
|
-
const next = new Set(prev);
|
|
120
|
-
if (selected) {
|
|
121
|
-
next.add(id);
|
|
122
|
-
} else {
|
|
123
|
-
next.delete(id);
|
|
124
|
-
}
|
|
125
|
-
return next;
|
|
126
|
-
});
|
|
127
|
-
}, []);
|
|
128
|
-
|
|
129
|
-
const handleSelectAll = useCallback(() => {
|
|
130
|
-
if (selectedIds.size === entries.length && entries.length > 0) {
|
|
131
|
-
setSelectedIds(new Set());
|
|
132
|
-
} else {
|
|
133
|
-
setSelectedIds(new Set(entries.map((e) => e._id)));
|
|
134
|
-
}
|
|
135
|
-
}, [selectedIds.size, entries]);
|
|
136
|
-
|
|
137
109
|
const handleClearSelection = useCallback(() => {
|
|
138
110
|
setSelectedIds(new Set());
|
|
139
111
|
}, []);
|
|
140
112
|
|
|
113
|
+
type Entry = (typeof entries)[number];
|
|
114
|
+
|
|
115
|
+
const entryColumns: CmsTableColumn<Entry>[] = useMemo(
|
|
116
|
+
() => [
|
|
117
|
+
{
|
|
118
|
+
key: "title",
|
|
119
|
+
header: "Title",
|
|
120
|
+
cell: (entry) => (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
124
|
+
className="block text-left"
|
|
125
|
+
>
|
|
126
|
+
<span className="font-medium text-foreground hover:text-primary">
|
|
127
|
+
{getEntryTitle(entry, entry.contentTypeName)}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="block text-xs text-muted-foreground">
|
|
130
|
+
{entry.slug}
|
|
131
|
+
</span>
|
|
132
|
+
</button>
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: "type",
|
|
137
|
+
header: "Type",
|
|
138
|
+
cell: (entry) => (
|
|
139
|
+
<span className="text-sm text-muted-foreground">
|
|
140
|
+
{getContentTypeDisplayName(entry.contentTypeName)}
|
|
141
|
+
</span>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "status",
|
|
146
|
+
header: "Status",
|
|
147
|
+
cell: (entry) => <CmsStatusBadge status={entry.status as ContentStatus} />,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
key: "updated",
|
|
151
|
+
header: "Updated",
|
|
152
|
+
cell: (entry) => (
|
|
153
|
+
<span className="text-sm text-muted-foreground">
|
|
154
|
+
{formatDate(entry._creationTime)}
|
|
155
|
+
</span>
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
key: "actions",
|
|
160
|
+
header: "Actions",
|
|
161
|
+
cell: (entry) => (
|
|
162
|
+
<CmsButton
|
|
163
|
+
variant="outline"
|
|
164
|
+
size="sm"
|
|
165
|
+
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
166
|
+
>
|
|
167
|
+
{canUpdate("contentEntries") ? "Edit" : "View"}
|
|
168
|
+
</CmsButton>
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
[navigation, contentTypes, canUpdate]
|
|
173
|
+
);
|
|
174
|
+
|
|
141
175
|
if (isLoading) {
|
|
142
176
|
return (
|
|
143
177
|
<div className="space-y-6 p-6">
|
|
@@ -162,56 +196,42 @@ export function ContentPage({ api, navigation }: ContentPageProps) {
|
|
|
162
196
|
description="Browse and manage content entries across all content types."
|
|
163
197
|
/>
|
|
164
198
|
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<SelectTrigger className="w-36">
|
|
202
|
-
<SelectValue placeholder="All Statuses" />
|
|
203
|
-
</SelectTrigger>
|
|
204
|
-
<SelectContent>
|
|
205
|
-
<SelectItem value="all">All Statuses</SelectItem>
|
|
206
|
-
<SelectItem value="draft">Draft</SelectItem>
|
|
207
|
-
<SelectItem value="published">Published</SelectItem>
|
|
208
|
-
<SelectItem value="scheduled">Scheduled</SelectItem>
|
|
209
|
-
<SelectItem value="archived">Archived</SelectItem>
|
|
210
|
-
</SelectContent>
|
|
211
|
-
</Select>
|
|
212
|
-
</div>
|
|
213
|
-
}
|
|
214
|
-
right={
|
|
199
|
+
<CmsFilterBar
|
|
200
|
+
search={{
|
|
201
|
+
value: searchQuery,
|
|
202
|
+
onChange: setSearchQuery,
|
|
203
|
+
placeholder: "Search content...",
|
|
204
|
+
className: "w-64",
|
|
205
|
+
}}
|
|
206
|
+
filters={[
|
|
207
|
+
{
|
|
208
|
+
key: "contentType",
|
|
209
|
+
value: selectedTypeId || "all",
|
|
210
|
+
onChange: (v) => setSelectedTypeId(v === "all" ? "" : v),
|
|
211
|
+
options: [
|
|
212
|
+
{ value: "all", label: "All Content Types" },
|
|
213
|
+
...contentTypes.map((type) => ({
|
|
214
|
+
value: type._id,
|
|
215
|
+
label: type.displayName,
|
|
216
|
+
})),
|
|
217
|
+
],
|
|
218
|
+
className: "w-48",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
key: "status",
|
|
222
|
+
value: selectedStatus || "all",
|
|
223
|
+
onChange: (v) => setSelectedStatus(v === "all" ? "" : (v as ContentStatus)),
|
|
224
|
+
options: [
|
|
225
|
+
{ value: "all", label: "All Statuses" },
|
|
226
|
+
{ value: "draft", label: "Draft" },
|
|
227
|
+
{ value: "published", label: "Published" },
|
|
228
|
+
{ value: "scheduled", label: "Scheduled" },
|
|
229
|
+
{ value: "archived", label: "Archived" },
|
|
230
|
+
],
|
|
231
|
+
className: "w-36",
|
|
232
|
+
},
|
|
233
|
+
]}
|
|
234
|
+
actions={
|
|
215
235
|
canCreate("contentEntries") && (
|
|
216
236
|
<DropdownMenu>
|
|
217
237
|
<DropdownMenuTrigger asChild>
|
|
@@ -253,91 +273,15 @@ export function ContentPage({ api, navigation }: ContentPageProps) {
|
|
|
253
273
|
}
|
|
254
274
|
/>
|
|
255
275
|
) : (
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
onCheckedChange={handleSelectAll}
|
|
266
|
-
aria-label="Select all entries"
|
|
267
|
-
/>
|
|
268
|
-
</th>
|
|
269
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
270
|
-
Title
|
|
271
|
-
</th>
|
|
272
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
273
|
-
Type
|
|
274
|
-
</th>
|
|
275
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
276
|
-
Status
|
|
277
|
-
</th>
|
|
278
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
279
|
-
Updated
|
|
280
|
-
</th>
|
|
281
|
-
<th className="p-3 text-left text-sm font-medium text-muted-foreground">
|
|
282
|
-
Actions
|
|
283
|
-
</th>
|
|
284
|
-
</tr>
|
|
285
|
-
</thead>
|
|
286
|
-
<tbody>
|
|
287
|
-
{entries.map((entry) => (
|
|
288
|
-
<tr
|
|
289
|
-
key={entry._id}
|
|
290
|
-
className={cn(
|
|
291
|
-
"border-b last:border-0 transition-colors hover:bg-muted/50",
|
|
292
|
-
selectedIds.has(entry._id) && "bg-primary/5"
|
|
293
|
-
)}
|
|
294
|
-
>
|
|
295
|
-
<td className="p-3">
|
|
296
|
-
<Checkbox
|
|
297
|
-
checked={selectedIds.has(entry._id)}
|
|
298
|
-
onCheckedChange={(checked) =>
|
|
299
|
-
handleSelectItem(entry._id, checked as boolean)
|
|
300
|
-
}
|
|
301
|
-
aria-label={`Select ${getEntryTitle(entry, entry.contentTypeName)}`}
|
|
302
|
-
/>
|
|
303
|
-
</td>
|
|
304
|
-
<td className="p-3">
|
|
305
|
-
<button
|
|
306
|
-
type="button"
|
|
307
|
-
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
308
|
-
className="block text-left"
|
|
309
|
-
>
|
|
310
|
-
<span className="font-medium text-foreground hover:text-primary">
|
|
311
|
-
{getEntryTitle(entry, entry.contentTypeName)}
|
|
312
|
-
</span>
|
|
313
|
-
<span className="block text-xs text-muted-foreground">
|
|
314
|
-
{entry.slug}
|
|
315
|
-
</span>
|
|
316
|
-
</button>
|
|
317
|
-
</td>
|
|
318
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
319
|
-
{getContentTypeDisplayName(entry.contentTypeName)}
|
|
320
|
-
</td>
|
|
321
|
-
<td className="p-3">
|
|
322
|
-
<CmsStatusBadge status={entry.status as ContentStatus} />
|
|
323
|
-
</td>
|
|
324
|
-
<td className="p-3 text-sm text-muted-foreground">
|
|
325
|
-
{formatDate(entry._creationTime)}
|
|
326
|
-
</td>
|
|
327
|
-
<td className="p-3">
|
|
328
|
-
<CmsButton
|
|
329
|
-
variant="outline"
|
|
330
|
-
size="sm"
|
|
331
|
-
onClick={() => navigation.navigateToEntry(entry._id)}
|
|
332
|
-
>
|
|
333
|
-
{canUpdate("contentEntries") ? "Edit" : "View"}
|
|
334
|
-
</CmsButton>
|
|
335
|
-
</td>
|
|
336
|
-
</tr>
|
|
337
|
-
))}
|
|
338
|
-
</tbody>
|
|
339
|
-
</table>
|
|
340
|
-
</div>
|
|
276
|
+
<CmsTable
|
|
277
|
+
columns={entryColumns}
|
|
278
|
+
data={entries}
|
|
279
|
+
getRowId={(e) => e._id}
|
|
280
|
+
selectable
|
|
281
|
+
selectedIds={selectedIds}
|
|
282
|
+
onSelectionChange={setSelectedIds}
|
|
283
|
+
emptyMessage="No content entries found"
|
|
284
|
+
/>
|
|
341
285
|
)}
|
|
342
286
|
</div>
|
|
343
287
|
);
|