@dyrected/admin 1.0.1
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/CHANGELOG.md +40 -0
- package/LICENSE.md +50 -0
- package/README.md +73 -0
- package/components.json +17 -0
- package/eslint.config.js +22 -0
- package/index.html +13 -0
- package/package.json +99 -0
- package/postcss.config.js +6 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.css +184 -0
- package/src/App.tsx +25 -0
- package/src/assets/dyrected.svg +155 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/auth/auth-gate.tsx +64 -0
- package/src/components/error-boundary.tsx +45 -0
- package/src/components/forms/field-renderer.tsx +111 -0
- package/src/components/forms/fields/block-builder.tsx +213 -0
- package/src/components/forms/fields/date-picker.tsx +60 -0
- package/src/components/forms/fields/json-editor.tsx +62 -0
- package/src/components/forms/fields/media-picker.tsx +286 -0
- package/src/components/forms/fields/multi-select.tsx +145 -0
- package/src/components/forms/fields/radio-field.tsx +51 -0
- package/src/components/forms/fields/relationship-picker.tsx +143 -0
- package/src/components/forms/fields/rich-text-editor.tsx +224 -0
- package/src/components/forms/fields/select-field.tsx +35 -0
- package/src/components/forms/fields/switch-field.tsx +16 -0
- package/src/components/forms/fields/text-area-field.tsx +15 -0
- package/src/components/forms/fields/text-field.tsx +24 -0
- package/src/components/forms/form-engine.tsx +87 -0
- package/src/components/forms/form-field-renderer.tsx +269 -0
- package/src/components/forms/utils.ts +97 -0
- package/src/components/layout/admin-shell.tsx +479 -0
- package/src/components/layout/branding-provider.tsx +112 -0
- package/src/components/live-preview/LivePreviewPane.tsx +128 -0
- package/src/components/media/focal-point-picker.tsx +66 -0
- package/src/components/media/media-card.tsx +44 -0
- package/src/components/media/media-grid.tsx +32 -0
- package/src/components/media/media-library-dialog.tsx +465 -0
- package/src/components/ui/aspect-ratio.tsx +7 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/calendar.tsx +214 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/checkbox.tsx +28 -0
- package/src/components/ui/command.tsx +151 -0
- package/src/components/ui/data-table.tsx +219 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/form.tsx +178 -0
- package/src/components/ui/input.tsx +24 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/page-header.tsx +30 -0
- package/src/components/ui/pagination.tsx +57 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/progress.tsx +26 -0
- package/src/components/ui/radio-group.tsx +42 -0
- package/src/components/ui/render-cell.tsx +110 -0
- package/src/components/ui/scroll-area.tsx +46 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +29 -0
- package/src/components/ui/sheet.tsx +140 -0
- package/src/components/ui/sidebar.tsx +771 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-preferences.ts +56 -0
- package/src/index.css +111 -0
- package/src/index.tsx +198 -0
- package/src/lib/utils.ts +32 -0
- package/src/main.tsx +10 -0
- package/src/pages/auth/first-user-page.tsx +115 -0
- package/src/pages/auth/login-page.tsx +91 -0
- package/src/pages/collections/edit-page.tsx +280 -0
- package/src/pages/collections/list-page.tsx +343 -0
- package/src/pages/dashboard/dashboard.tsx +150 -0
- package/src/pages/globals/editor-page.tsx +122 -0
- package/src/pages/media/media-page.tsx +564 -0
- package/src/pages/setup/setup-prompt.tsx +152 -0
- package/src/providers/dyrected-provider.tsx +122 -0
- package/src/providers/query-provider.tsx +19 -0
- package/src/types/jexl.d.ts +11 -0
- package/tailwind.config.ts +102 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +12 -0
- package/tsconfig.node.json +27 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Database, Globe, ImageIcon, ArrowRight } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { useQuery, useQueries } from "@tanstack/react-query";
|
|
4
|
+
import { useDyrected } from "../../providers/dyrected-provider";
|
|
5
|
+
import { Button } from "../../components/ui/button";
|
|
6
|
+
|
|
7
|
+
export function Dashboard() {
|
|
8
|
+
const { client } = useDyrected();
|
|
9
|
+
|
|
10
|
+
const { data: schemas, isLoading: isLoadingSchemas } = useQuery({
|
|
11
|
+
queryKey: ["schemas"],
|
|
12
|
+
queryFn: () => client!.getSchemas(),
|
|
13
|
+
enabled: !!client,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const collections = (schemas?.collections || []).filter((c: any) => !c.admin?.hidden && !c.slug.startsWith('platform_'));
|
|
17
|
+
const globals = (schemas?.globals || []).filter((g: any) => !g.admin?.hidden && !g.slug.startsWith('platform_'));
|
|
18
|
+
|
|
19
|
+
const collectionCounts = useQueries({
|
|
20
|
+
queries: collections.map((col: any) => ({
|
|
21
|
+
queryKey: ["collection-count", col.slug],
|
|
22
|
+
queryFn: () => client!.find(col.slug, { limit: 1 }),
|
|
23
|
+
enabled: !!client && !!col.slug,
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (isLoadingSchemas) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex items-center justify-center h-64">
|
|
30
|
+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// No schema yet — route to /setup
|
|
36
|
+
if (collections.length === 0 && globals.length === 0) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex items-center justify-center h-64">
|
|
39
|
+
<div className="text-center space-y-4">
|
|
40
|
+
<p className="text-muted-foreground">No collections configured yet.</p>
|
|
41
|
+
<Button asChild>
|
|
42
|
+
<Link to="/setup">View Integration Guide</Link>
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="space-y-8 animate-in fade-in duration-500">
|
|
51
|
+
<div>
|
|
52
|
+
<h2 className="text-2xl font-semibold tracking-tight">Overview</h2>
|
|
53
|
+
<p className="text-muted-foreground">Monitor and manage your site's content and structure.</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
57
|
+
<div className="p-1 space-y-2 group border border-card">
|
|
58
|
+
<div className="flex items-center gap-3">
|
|
59
|
+
<div className="rounded-md bg-primary/5 p-1.5 text-primary/60 group-hover:bg-primary/10 group-hover:text-primary transition-colors">
|
|
60
|
+
<Database className="h-4 w-4" />
|
|
61
|
+
</div>
|
|
62
|
+
<h3 className="text-[10px] font-bold text-muted-foreground/40 uppercase tracking-widest">Collections</h3>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-3xl font-bold tracking-tight">{collections.length}</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="p-1 space-y-2 group border border-card">
|
|
68
|
+
<div className="flex items-center gap-3">
|
|
69
|
+
<div className="rounded-md bg-secondary/5 p-1.5 text-muted-foreground/60 group-hover:bg-accent group-hover:text-foreground transition-colors">
|
|
70
|
+
<Globe className="h-4 w-4" />
|
|
71
|
+
</div>
|
|
72
|
+
<h3 className="text-[10px] font-bold text-muted-foreground/40 uppercase tracking-widest">Global Configs</h3>
|
|
73
|
+
</div>
|
|
74
|
+
<p className="text-3xl font-bold tracking-tight">{globals.length}</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="p-1 space-y-2 group border border-card">
|
|
78
|
+
<div className="flex items-center gap-3">
|
|
79
|
+
<div className="rounded-md bg-accent p-1.5 text-muted-foreground/60 group-hover:bg-accent group-hover:text-foreground transition-colors">
|
|
80
|
+
<ImageIcon className="h-4 w-4" />
|
|
81
|
+
</div>
|
|
82
|
+
<h3 className="text-[10px] font-bold text-muted-foreground/40 uppercase tracking-widest">Media Files</h3>
|
|
83
|
+
</div>
|
|
84
|
+
<p className="text-3xl font-bold tracking-tight">-</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="grid gap-8 md:grid-cols-2">
|
|
89
|
+
<section >
|
|
90
|
+
<div className="flex items-center justify-between mb-4">
|
|
91
|
+
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
92
|
+
<Database className="h-5 w-5 text-primary" />
|
|
93
|
+
Collections
|
|
94
|
+
</h3>
|
|
95
|
+
{/* <Button variant="ghost" size="sm" asChild className="text-xs">
|
|
96
|
+
<Link to="/collections">View All</Link>
|
|
97
|
+
</Button> */}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="space-y-1 border border-card">
|
|
100
|
+
{collections.map((col: any, idx: number) => (
|
|
101
|
+
<Link
|
|
102
|
+
key={col.slug}
|
|
103
|
+
to={`/collections/${col.slug}`}
|
|
104
|
+
className="group flex items-center justify-between p-3 rounded-md hover:bg-primary/[0.02] transition-colors"
|
|
105
|
+
>
|
|
106
|
+
<div>
|
|
107
|
+
<p className="font-medium group-hover:text-primary transition-colors">{col.labels?.plural || col.slug}</p>
|
|
108
|
+
<p className="text-xs text-muted-foreground uppercase">{col.slug}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex items-center gap-3">
|
|
111
|
+
<div className="text-right mr-4">
|
|
112
|
+
<p className="text-sm font-semibold">{collectionCounts[idx]?.data?.total || 0}</p>
|
|
113
|
+
<p className="text-[10px] text-muted-foreground uppercase">Entries</p>
|
|
114
|
+
</div>
|
|
115
|
+
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-transform group-hover:translate-x-0.5" />
|
|
116
|
+
</div>
|
|
117
|
+
</Link>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</section>
|
|
121
|
+
|
|
122
|
+
<section>
|
|
123
|
+
<div className="flex items-center justify-between mb-4">
|
|
124
|
+
<h3 className="text-lg font-semibold flex items-center gap-2">
|
|
125
|
+
<Globe className="h-5 w-5 text-secondary-foreground" />
|
|
126
|
+
Global Settings
|
|
127
|
+
</h3>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="space-y-1 border border-card">
|
|
130
|
+
{globals.map((glb: any) => (
|
|
131
|
+
<Link
|
|
132
|
+
key={glb.slug}
|
|
133
|
+
to={`/globals/${glb.slug}`}
|
|
134
|
+
className="group flex items-center justify-between p-3 rounded-md hover:bg-primary/[0.02] transition-colors"
|
|
135
|
+
>
|
|
136
|
+
<div>
|
|
137
|
+
<p className="font-medium group-hover:text-secondary-foreground transition-colors">{glb.label || glb.slug}</p>
|
|
138
|
+
<p className="text-xs text-muted-foreground uppercase">{glb.slug}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<div className="bg-secondary/10 px-2 py-1 rounded text-[10px] font-bold text-secondary-foreground uppercase">
|
|
141
|
+
Global
|
|
142
|
+
</div>
|
|
143
|
+
</Link>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
2
|
+
import { toast } from "sonner"
|
|
3
|
+
import { useDyrected } from "../../providers/dyrected-provider"
|
|
4
|
+
import { FormEngine } from "../../components/forms/form-engine"
|
|
5
|
+
import { useParams } from "react-router-dom"
|
|
6
|
+
import { Globe, Save } from "lucide-react"
|
|
7
|
+
import { useState, useEffect } from "react"
|
|
8
|
+
import { Button } from "../../components/ui/button"
|
|
9
|
+
|
|
10
|
+
export function GlobalEditorPage() {
|
|
11
|
+
const { slug } = useParams()
|
|
12
|
+
const { client } = useDyrected()
|
|
13
|
+
const queryClient = useQueryClient()
|
|
14
|
+
const [isDirty, setIsDirty] = useState(false)
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
18
|
+
if (isDirty) {
|
|
19
|
+
e.preventDefault()
|
|
20
|
+
e.returnValue = ""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
window.addEventListener("beforeunload", handleBeforeUnload)
|
|
24
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload)
|
|
25
|
+
}, [isDirty])
|
|
26
|
+
|
|
27
|
+
// Fetch schema
|
|
28
|
+
const { data: schemas } = useQuery({
|
|
29
|
+
queryKey: ["schemas"],
|
|
30
|
+
queryFn: () => client!.getSchemas(),
|
|
31
|
+
enabled: !!client,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const schema = schemas?.globals.find((g: any) => g.slug === slug)
|
|
35
|
+
|
|
36
|
+
// Fetch global data
|
|
37
|
+
const { data: globalData, isLoading: isGlobalLoading } = useQuery({
|
|
38
|
+
queryKey: ["global", slug],
|
|
39
|
+
queryFn: () => client!.getGlobal(slug!),
|
|
40
|
+
enabled: !!client && !!slug,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// Cmd+S to save
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleSave = (e: KeyboardEvent) => {
|
|
46
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
47
|
+
e.preventDefault()
|
|
48
|
+
document.getElementById('dyrected-form-submit')?.click()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
window.addEventListener("keydown", handleSave)
|
|
52
|
+
return () => window.removeEventListener("keydown", handleSave)
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const saveMutation = useMutation({
|
|
56
|
+
mutationFn: (data: any) => {
|
|
57
|
+
return client!.updateGlobal(slug!, data)
|
|
58
|
+
},
|
|
59
|
+
onSuccess: () => {
|
|
60
|
+
setIsDirty(false)
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: ["global", slug] })
|
|
62
|
+
toast.success(`${schema.label || schema.slug} updated successfully`)
|
|
63
|
+
},
|
|
64
|
+
onError: (error: any) => {
|
|
65
|
+
toast.error("Failed to update settings", {
|
|
66
|
+
description: error.message
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (!schema) return <div>Global schema not found for: {slug}</div>
|
|
72
|
+
if (isGlobalLoading) return <div>Loading global settings...</div>
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-8 max-w-5xl mx-auto">
|
|
76
|
+
<div className="flex items-center justify-between gap-4 border-b border-border/50 pb-6">
|
|
77
|
+
<div className="flex items-center gap-4">
|
|
78
|
+
<div className="p-2 bg-primary/10 text-primary rounded-lg shrink-0">
|
|
79
|
+
<Globe className="h-5 w-5" />
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<h1 className="text-lg font-serif font-bold tracking-tight text-foreground truncate">
|
|
83
|
+
{schema.label || schema.slug}
|
|
84
|
+
</h1>
|
|
85
|
+
<p className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/40 leading-none mt-1">
|
|
86
|
+
Global Configuration
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<Button
|
|
93
|
+
size="icon"
|
|
94
|
+
className="h-9 w-9 rounded-lg shadow-sm"
|
|
95
|
+
onClick={() => document.getElementById('dyrected-form-submit')?.click()}
|
|
96
|
+
disabled={saveMutation.isPending}
|
|
97
|
+
title="Save Changes (⌘S)"
|
|
98
|
+
>
|
|
99
|
+
{saveMutation.isPending ? (
|
|
100
|
+
<div className="h-4 w-4 animate-spin border-2 border-current border-t-transparent rounded-full" />
|
|
101
|
+
) : (
|
|
102
|
+
<Save className="h-4 w-4" />
|
|
103
|
+
)}
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="animate-in space-y-8 pb-20">
|
|
109
|
+
<FormEngine
|
|
110
|
+
collection={slug!}
|
|
111
|
+
fields={schema.fields}
|
|
112
|
+
defaultValues={globalData || {}}
|
|
113
|
+
onSubmit={(data) => saveMutation.mutate(data)}
|
|
114
|
+
isLoading={saveMutation.isPending}
|
|
115
|
+
onChange={(dirty) => setIsDirty(dirty)}
|
|
116
|
+
submitLabel="Save Changes"
|
|
117
|
+
/>
|
|
118
|
+
<button id="dyrected-form-submit" type="submit" className="hidden" />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|