kilo-cms 0.1.0
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 +186 -0
- package/drizzle.kilo.config.ts +17 -0
- package/package.json +97 -0
- package/src/admin/AiSettingsEditor.tsx +48 -0
- package/src/admin/AiWritingAssistant.tsx +159 -0
- package/src/admin/ApiTokensEditor.tsx +69 -0
- package/src/admin/ArrayField.tsx +118 -0
- package/src/admin/BlocksField.tsx +131 -0
- package/src/admin/Button.tsx +31 -0
- package/src/admin/CloseButton.tsx +18 -0
- package/src/admin/CmsLocalesProvider.tsx +19 -0
- package/src/admin/CmsSidebar.tsx +147 -0
- package/src/admin/CmsSiteSettingsProvider.tsx +30 -0
- package/src/admin/CmsThemeProvider.tsx +100 -0
- package/src/admin/CollectionListEditor.tsx +278 -0
- package/src/admin/CollectionRecordEditor.tsx +420 -0
- package/src/admin/ContentEditorForm.tsx +21 -0
- package/src/admin/ContentViewSettings.tsx +55 -0
- package/src/admin/DashboardBuilder.tsx +284 -0
- package/src/admin/DragHandle.tsx +3 -0
- package/src/admin/DynamicField.tsx +517 -0
- package/src/admin/EditorHeader.tsx +13 -0
- package/src/admin/EditorSkeleton.tsx +27 -0
- package/src/admin/EmailSettingsEditor.tsx +80 -0
- package/src/admin/Field.tsx +215 -0
- package/src/admin/FilterBuilder.tsx +62 -0
- package/src/admin/GroupField.tsx +61 -0
- package/src/admin/ImageUploader.tsx +198 -0
- package/src/admin/ListingSettingsModal.tsx +68 -0
- package/src/admin/ListingView.tsx +123 -0
- package/src/admin/LocalesEditor.tsx +86 -0
- package/src/admin/LocalizedField.tsx +46 -0
- package/src/admin/MapPicker.tsx +53 -0
- package/src/admin/MediaLibrary.tsx +483 -0
- package/src/admin/MediaUploader.tsx +55 -0
- package/src/admin/Portal.tsx +10 -0
- package/src/admin/PublishingPanel.tsx +102 -0
- package/src/admin/RecordMeta.tsx +24 -0
- package/src/admin/ReviewQueueEditor.tsx +103 -0
- package/src/admin/RichTextEditor.tsx +49 -0
- package/src/admin/RoleEditor.tsx +121 -0
- package/src/admin/RolesEditor.tsx +42 -0
- package/src/admin/RowActionsMenu.tsx +64 -0
- package/src/admin/ScheduledEditor.tsx +155 -0
- package/src/admin/SeoSettingsEditor.tsx +53 -0
- package/src/admin/SingleRecordEditor.tsx +215 -0
- package/src/admin/SiteSettingsEditor.tsx +96 -0
- package/src/admin/Toast.tsx +45 -0
- package/src/admin/UserEditor.tsx +64 -0
- package/src/admin/UsersEditor.tsx +37 -0
- package/src/admin/WebhooksEditor.tsx +115 -0
- package/src/admin/admin.css +6637 -0
- package/src/admin/globals.css +14 -0
- package/src/admin/pages/admin-index.tsx +5 -0
- package/src/admin/pages/ai-settings.tsx +9 -0
- package/src/admin/pages/api-tokens.tsx +10 -0
- package/src/admin/pages/collection-list.tsx +22 -0
- package/src/admin/pages/collection-record.tsx +31 -0
- package/src/admin/pages/dashboard.tsx +10 -0
- package/src/admin/pages/email-settings.tsx +10 -0
- package/src/admin/pages/layout.tsx +24 -0
- package/src/admin/pages/locales-settings.tsx +10 -0
- package/src/admin/pages/login.tsx +25 -0
- package/src/admin/pages/media.tsx +9 -0
- package/src/admin/pages/review-queue.tsx +20 -0
- package/src/admin/pages/role-edit.tsx +11 -0
- package/src/admin/pages/roles.tsx +10 -0
- package/src/admin/pages/scheduled.tsx +17 -0
- package/src/admin/pages/seo-settings.tsx +9 -0
- package/src/admin/pages/setup-admin.tsx +76 -0
- package/src/admin/pages/single-record.tsx +24 -0
- package/src/admin/pages/site-settings.tsx +9 -0
- package/src/admin/pages/user-edit.tsx +11 -0
- package/src/admin/pages/user-new.tsx +10 -0
- package/src/admin/pages/users.tsx +10 -0
- package/src/admin/pages/webhooks-settings.tsx +10 -0
- package/src/admin/useKeyedRows.ts +35 -0
- package/src/ai-config.ts +35 -0
- package/src/ai-secrets.ts +17 -0
- package/src/ai-types.ts +10 -0
- package/src/ai-writer.ts +84 -0
- package/src/api-tokens.ts +25 -0
- package/src/auth-client.ts +5 -0
- package/src/auth.ts +33 -0
- package/src/calendar-grid.ts +12 -0
- package/src/cli/index.mjs +149 -0
- package/src/cms-appearance.ts +55 -0
- package/src/collections/content-view.ts +69 -0
- package/src/collections/define.ts +17 -0
- package/src/collections/filters.ts +92 -0
- package/src/collections/index.ts +11 -0
- package/src/collections/join.ts +64 -0
- package/src/collections/locale.ts +22 -0
- package/src/collections/query.ts +160 -0
- package/src/collections/registry.ts +88 -0
- package/src/collections/schedule.ts +12 -0
- package/src/collections/server.ts +12 -0
- package/src/collections/single.ts +26 -0
- package/src/collections/table.ts +14 -0
- package/src/collections/types.ts +336 -0
- package/src/collections/validation.ts +217 -0
- package/src/collections/values.ts +181 -0
- package/src/collections/workflow.ts +90 -0
- package/src/config.ts +34 -0
- package/src/content-types.ts +218 -0
- package/src/dashboard.ts +39 -0
- package/src/db.ts +50 -0
- package/src/email.ts +39 -0
- package/src/format.ts +4 -0
- package/src/listQuery.ts +23 -0
- package/src/locale-settings.ts +23 -0
- package/src/media-storage.ts +19 -0
- package/src/permissions.ts +37 -0
- package/src/publishing-workflow-db.ts +11 -0
- package/src/publishing-workflow.ts +20 -0
- package/src/richtext.ts +5 -0
- package/src/routes/admin/ai/settings/route.ts +20 -0
- package/src/routes/admin/ai/write/route.ts +21 -0
- package/src/routes/admin/api-tokens/[id]/route.ts +12 -0
- package/src/routes/admin/api-tokens/route.ts +26 -0
- package/src/routes/admin/collections/[collection]/[id]/approve/route.ts +22 -0
- package/src/routes/admin/collections/[collection]/[id]/duplicate/route.ts +36 -0
- package/src/routes/admin/collections/[collection]/[id]/restore/route.ts +19 -0
- package/src/routes/admin/collections/[collection]/[id]/review/route.ts +28 -0
- package/src/routes/admin/collections/[collection]/[id]/route.ts +110 -0
- package/src/routes/admin/collections/[collection]/[id]/schedule/route.ts +42 -0
- package/src/routes/admin/collections/[collection]/[id]/submit-review/route.ts +23 -0
- package/src/routes/admin/collections/[collection]/[id]/unschedule/route.ts +22 -0
- package/src/routes/admin/collections/[collection]/bulk/route.ts +61 -0
- package/src/routes/admin/collections/[collection]/options/route.ts +38 -0
- package/src/routes/admin/collections/[collection]/reorder/route.ts +19 -0
- package/src/routes/admin/collections/[collection]/route.ts +88 -0
- package/src/routes/admin/collections/route.ts +13 -0
- package/src/routes/admin/content-views/[type]/route.ts +25 -0
- package/src/routes/admin/dashboard/data/route.ts +39 -0
- package/src/routes/admin/dashboard/route.ts +30 -0
- package/src/routes/admin/email-settings/route.ts +49 -0
- package/src/routes/admin/email-settings/test/route.ts +18 -0
- package/src/routes/admin/locales/[code]/route.ts +38 -0
- package/src/routes/admin/locales/reorder/route.ts +18 -0
- package/src/routes/admin/locales/route.ts +46 -0
- package/src/routes/admin/media/[id]/restore/route.ts +13 -0
- package/src/routes/admin/media/[id]/route.ts +56 -0
- package/src/routes/admin/media/bulk/route.ts +46 -0
- package/src/routes/admin/media/folders/[id]/route.ts +34 -0
- package/src/routes/admin/media/route.ts +41 -0
- package/src/routes/admin/media/stats/route.ts +14 -0
- package/src/routes/admin/roles/[id]/route.ts +43 -0
- package/src/routes/admin/roles/route.ts +34 -0
- package/src/routes/admin/settings/route.ts +45 -0
- package/src/routes/admin/single/[type]/route.ts +54 -0
- package/src/routes/admin/singles/route.ts +15 -0
- package/src/routes/admin/upload/route.ts +55 -0
- package/src/routes/admin/users/[id]/route.ts +24 -0
- package/src/routes/admin/users/route.ts +24 -0
- package/src/routes/admin/webhooks/[id]/route.ts +38 -0
- package/src/routes/admin/webhooks/route.ts +26 -0
- package/src/routes/cron/publish-scheduled.ts +37 -0
- package/src/routes/setup/admin.ts +19 -0
- package/src/schema/admin-ui.ts +18 -0
- package/src/schema/auth.ts +49 -0
- package/src/schema/developer-tools.ts +44 -0
- package/src/schema/helpers.ts +35 -0
- package/src/schema/index.ts +33 -0
- package/src/schema/locales.ts +13 -0
- package/src/schema/media.ts +22 -0
- package/src/schema/rbac.ts +11 -0
- package/src/schema/settings.ts +28 -0
- package/src/secrets.ts +26 -0
- package/src/seo.ts +69 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/local.ts +27 -0
- package/src/storage/s3-compatible.ts +31 -0
- package/src/storage/types.ts +8 -0
- package/src/storage/vercel-blob.ts +17 -0
- package/src/useBodyScrollLock.ts +12 -0
- package/src/useLocalStorageState.ts +26 -0
- package/src/webhook-events.ts +12 -0
- package/src/webhooks.ts +30 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import * as echarts from 'echarts'
|
|
5
|
+
import { CmsSidebar } from './CmsSidebar'
|
|
6
|
+
import { EditorHeader } from './EditorHeader'
|
|
7
|
+
import { EditorSkeleton } from './EditorSkeleton'
|
|
8
|
+
import { Button } from './Button'
|
|
9
|
+
import { SelectField, TextField } from './Field'
|
|
10
|
+
import {
|
|
11
|
+
type DashboardConfig,
|
|
12
|
+
type DashboardMetric,
|
|
13
|
+
type DashboardSize,
|
|
14
|
+
type DashboardSource,
|
|
15
|
+
type DashboardVisual,
|
|
16
|
+
type DashboardWidget,
|
|
17
|
+
} from '../dashboard'
|
|
18
|
+
|
|
19
|
+
// Resolved server-side (the registry is never populated on the client, `defineKiloConfig()` is
|
|
20
|
+
// server-only) and passed down as a prop — same pattern as `CollectionRecordEditor`'s `config`.
|
|
21
|
+
type DashboardSourceOption = { value: DashboardSource; label: string; fields: string[] }
|
|
22
|
+
|
|
23
|
+
type Result = { value?: number; data?: Array<{ name: string; value: number }> }
|
|
24
|
+
|
|
25
|
+
const metricOptions = [
|
|
26
|
+
{ value: 'count', label: 'Count of records' },
|
|
27
|
+
{ value: 'true', label: 'Count where true' },
|
|
28
|
+
{ value: 'false', label: 'Count where false' },
|
|
29
|
+
{ value: 'sum', label: 'Sum of a number' },
|
|
30
|
+
{ value: 'avg', label: 'Average of a number' },
|
|
31
|
+
]
|
|
32
|
+
const visualOptions = [
|
|
33
|
+
{ value: 'number', label: 'Big number' },
|
|
34
|
+
{ value: 'bar', label: 'Bar chart' },
|
|
35
|
+
{ value: 'line', label: 'Line chart' },
|
|
36
|
+
{ value: 'donut', label: 'Donut chart' },
|
|
37
|
+
{ value: 'table', label: 'Table' },
|
|
38
|
+
]
|
|
39
|
+
const sizeOptions = [
|
|
40
|
+
{ value: 'small', label: 'Small' },
|
|
41
|
+
{ value: 'medium', label: 'Medium' },
|
|
42
|
+
{ value: 'large', label: 'Large (full width)' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
function Chart({ visual, data }: { visual: 'bar' | 'line' | 'donut'; data: Array<{ name: string; value: number }> }) {
|
|
46
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!ref.current) return
|
|
49
|
+
const chart = echarts.init(ref.current)
|
|
50
|
+
const names = data.map((item) => item.name)
|
|
51
|
+
const values = data.map((item) => item.value)
|
|
52
|
+
chart.setOption(
|
|
53
|
+
visual === 'donut'
|
|
54
|
+
? { tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['40%', '70%'], data: data.map((item) => ({ name: item.name, value: item.value })), label: { fontSize: 11 } }] }
|
|
55
|
+
: {
|
|
56
|
+
grid: { left: 8, right: 8, top: 8, bottom: 24, containLabel: true },
|
|
57
|
+
tooltip: { trigger: 'axis' },
|
|
58
|
+
xAxis: { type: 'category', data: names, axisLabel: { fontSize: 10 } },
|
|
59
|
+
yAxis: { type: 'value', axisLabel: { fontSize: 10 } },
|
|
60
|
+
series: [{ type: visual, data: values, itemStyle: { color: '#c8ff4d' }, lineStyle: { color: '#c8ff4d' }, smooth: visual === 'line' }],
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
const observer = new ResizeObserver(() => chart.resize())
|
|
64
|
+
observer.observe(ref.current)
|
|
65
|
+
return () => { observer.disconnect(); chart.dispose() }
|
|
66
|
+
}, [visual, data])
|
|
67
|
+
return <div ref={ref} className="cms-dashboard-chart" />
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Widget({ widget }: { widget: DashboardWidget }) {
|
|
71
|
+
const [result, setResult] = useState<Result | null>(null)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setResult(null)
|
|
74
|
+
fetch('/api/admin/dashboard/data', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(widget) })
|
|
75
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
76
|
+
.then(setResult)
|
|
77
|
+
.catch(() => setResult(null))
|
|
78
|
+
}, [widget])
|
|
79
|
+
const data = result?.data ?? []
|
|
80
|
+
const max = Math.max(...data.map((item) => item.value), 1)
|
|
81
|
+
return (
|
|
82
|
+
<article className="cms-dashboard-widget">
|
|
83
|
+
<header>
|
|
84
|
+
<strong>{widget.title}</strong>
|
|
85
|
+
<span>{widget.source}</span>
|
|
86
|
+
</header>
|
|
87
|
+
{result === null ? (
|
|
88
|
+
<p className="cms-dashboard-loading">Loading data…</p>
|
|
89
|
+
) : widget.visual === 'number' ? (
|
|
90
|
+
<div className="cms-dashboard-number">{result.value ?? 0}</div>
|
|
91
|
+
) : widget.visual === 'table' ? (
|
|
92
|
+
<div className="cms-dashboard-widget-table">
|
|
93
|
+
{data.map((item) => (
|
|
94
|
+
<div key={item.name}>
|
|
95
|
+
<span>{item.name}</span>
|
|
96
|
+
<strong>{item.value}</strong>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
) : widget.visual === 'bar' && data.length <= 1 ? (
|
|
101
|
+
<div className="cms-dashboard-bars">
|
|
102
|
+
{data.map((item) => (
|
|
103
|
+
<div className="cms-dashboard-bar-row" key={item.name}>
|
|
104
|
+
<span>{item.name}</span>
|
|
105
|
+
<div><i style={{ width: `${Math.max((item.value / max) * 100, 4)}%` }} /></div>
|
|
106
|
+
<strong>{item.value}</strong>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
) : (
|
|
111
|
+
<Chart visual={widget.visual} data={data} />
|
|
112
|
+
)}
|
|
113
|
+
</article>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function WidgetConfig({ widget, dashboardSources, onChange, onRemove, onMove, canMoveUp, canMoveDown }: {
|
|
118
|
+
widget: DashboardWidget
|
|
119
|
+
dashboardSources: DashboardSourceOption[]
|
|
120
|
+
onChange: (patch: Partial<DashboardWidget>) => void
|
|
121
|
+
onRemove: () => void
|
|
122
|
+
onMove: (direction: 'up' | 'down') => void
|
|
123
|
+
canMoveUp: boolean
|
|
124
|
+
canMoveDown: boolean
|
|
125
|
+
}) {
|
|
126
|
+
const source = dashboardSources.find((item) => item.value === widget.source) ?? dashboardSources[0]
|
|
127
|
+
if (!source) return null
|
|
128
|
+
const needsField = widget.metric !== 'count'
|
|
129
|
+
const fieldOptions = [{ value: '', label: 'Choose a field' }, ...source.fields.map((field) => ({ value: field, label: field }))]
|
|
130
|
+
const groupByOptions = [{ value: '', label: 'One total value' }, ...source.fields.map((field) => ({ value: field, label: field }))]
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<article className="cms-dashboard-widget-config">
|
|
134
|
+
<header>
|
|
135
|
+
<div>
|
|
136
|
+
<span className="cms-dashboard-config-kicker">Widget</span>
|
|
137
|
+
<strong>Configure this card</strong>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="cms-dashboard-config-actions">
|
|
140
|
+
<Button variant="secondary" iconOnly compact disabled={!canMoveUp} onClick={() => onMove('up')} aria-label="Move up" icon="↑" />
|
|
141
|
+
<Button variant="secondary" iconOnly compact disabled={!canMoveDown} onClick={() => onMove('down')} aria-label="Move down" icon="↓" />
|
|
142
|
+
<Button variant="negative" compact onClick={onRemove}>Remove</Button>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
<div className="cms-dashboard-config-grid">
|
|
146
|
+
<TextField label="Title" value={widget.title} onChange={(event) => onChange({ title: event.target.value })} placeholder="e.g. Published projects" />
|
|
147
|
+
<SelectField label="Content source" value={widget.source} options={dashboardSources.map((item) => ({ value: item.value, label: item.label }))} onChange={(event) => onChange({ source: event.target.value as DashboardSource, groupBy: undefined, field: undefined })} />
|
|
148
|
+
<SelectField label="Measure" value={widget.metric} options={metricOptions} onChange={(event) => onChange({ metric: event.target.value as DashboardMetric })} />
|
|
149
|
+
{needsField && <SelectField label="Field" value={widget.field ?? ''} options={fieldOptions} onChange={(event) => onChange({ field: event.target.value || undefined })} />}
|
|
150
|
+
<SelectField label="Break down by" value={widget.groupBy ?? ''} options={groupByOptions} onChange={(event) => onChange({ groupBy: event.target.value || undefined })} />
|
|
151
|
+
<SelectField label="Display as" value={widget.visual} options={visualOptions} onChange={(event) => onChange({ visual: event.target.value as DashboardVisual })} />
|
|
152
|
+
<SelectField label="Size" value={widget.size} options={sizeOptions} onChange={(event) => onChange({ size: event.target.value as DashboardSize })} />
|
|
153
|
+
<TextField label="Show top" type="number" min="1" max="20" value={widget.limit} onChange={(event) => onChange({ limit: Math.max(1, Math.min(20, Number(event.target.value) || 1)) })} />
|
|
154
|
+
</div>
|
|
155
|
+
<p className="cms-dashboard-config-help">
|
|
156
|
+
{widget.metric === 'count' && !widget.groupBy
|
|
157
|
+
? 'Shows one total count for the selected content source.'
|
|
158
|
+
: widget.groupBy
|
|
159
|
+
? `Groups records by ${widget.groupBy} and shows the highest values first.`
|
|
160
|
+
: 'Choose a field to calculate the selected measure.'}
|
|
161
|
+
</p>
|
|
162
|
+
</article>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function DashboardBuilder({ userName, canUpdate, dashboardSources }: { userName: string; canUpdate: boolean; dashboardSources: DashboardSourceOption[] }) {
|
|
167
|
+
const [config, setConfig] = useState<DashboardConfig | null>(null)
|
|
168
|
+
const [loading, setLoading] = useState(true)
|
|
169
|
+
const [editing, setEditing] = useState(false)
|
|
170
|
+
const [saving, setSaving] = useState(false)
|
|
171
|
+
const [message, setMessage] = useState('')
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
fetch('/api/admin/dashboard')
|
|
175
|
+
.then((response) => (response.ok ? response.json() : Promise.reject(new Error('dashboard request failed'))))
|
|
176
|
+
.then((data) => setConfig(data))
|
|
177
|
+
.catch(() => { setConfig({ widgets: [] }); setMessage('Could not load dashboard configuration.') })
|
|
178
|
+
.finally(() => setLoading(false))
|
|
179
|
+
}, [])
|
|
180
|
+
|
|
181
|
+
if (loading || !config) return <main className="cms-dashboard"><CmsSidebar active="dashboard" userName={userName} /><section className="cms-content"><EditorHeader compact title="Dashboard" meta="Loading…" /><EditorSkeleton sections={1} /></section></main>
|
|
182
|
+
|
|
183
|
+
function addWidget() {
|
|
184
|
+
const id = crypto.randomUUID()
|
|
185
|
+
const source = dashboardSources[0]?.value ?? ''
|
|
186
|
+
setConfig((current) => current ? ({ widgets: [...current.widgets, { id, title: 'New widget', source, metric: 'count', visual: 'number', limit: 5, size: 'medium' }] }) : current)
|
|
187
|
+
}
|
|
188
|
+
function updateWidget(id: string, patch: Partial<DashboardWidget>) {
|
|
189
|
+
setConfig((current) => current ? ({ widgets: current.widgets.map((widget) => (widget.id === id ? { ...widget, ...patch } : widget)) }) : current)
|
|
190
|
+
}
|
|
191
|
+
function removeWidget(id: string) {
|
|
192
|
+
setConfig((current) => current ? ({ widgets: current.widgets.filter((widget) => widget.id !== id) }) : current)
|
|
193
|
+
}
|
|
194
|
+
function moveWidget(id: string, direction: 'up' | 'down') {
|
|
195
|
+
setConfig((current) => {
|
|
196
|
+
if (!current) return current
|
|
197
|
+
const index = current.widgets.findIndex((widget) => widget.id === id)
|
|
198
|
+
const target = direction === 'up' ? index - 1 : index + 1
|
|
199
|
+
if (index < 0 || target < 0 || target >= current.widgets.length) return current
|
|
200
|
+
const widgets = [...current.widgets]
|
|
201
|
+
;[widgets[index], widgets[target]] = [widgets[target], widgets[index]]
|
|
202
|
+
return { widgets }
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
async function save() {
|
|
206
|
+
setSaving(true)
|
|
207
|
+
setMessage('')
|
|
208
|
+
const response = await fetch('/api/admin/dashboard', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) })
|
|
209
|
+
setSaving(false)
|
|
210
|
+
if (!response.ok) { setMessage('Could not save dashboard.'); return }
|
|
211
|
+
setEditing(false)
|
|
212
|
+
setMessage('Dashboard saved.')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<main className="cms-dashboard">
|
|
217
|
+
<CmsSidebar active="dashboard" userName={userName} />
|
|
218
|
+
<section className="cms-content">
|
|
219
|
+
<EditorHeader
|
|
220
|
+
compact
|
|
221
|
+
title="Dashboard"
|
|
222
|
+
meta={`${config.widgets.length} widgets`}
|
|
223
|
+
actions={canUpdate ? <Button variant={editing ? 'secondary' : 'positive'} type="button" onClick={() => setEditing((value) => !value)} icon="↗">{editing ? 'Close builder' : 'Build dashboard'}</Button> : undefined}
|
|
224
|
+
/>
|
|
225
|
+
{message && <p className="cms-dashboard-message">{message}</p>}
|
|
226
|
+
{!config.widgets.length && (
|
|
227
|
+
<div className="cms-dashboard-intro">
|
|
228
|
+
<div>
|
|
229
|
+
<p className="section-label">Workspace overview</p>
|
|
230
|
+
<h2>See what is happening across your content.</h2>
|
|
231
|
+
<p>Build a dashboard from your Services, Projects, and Blog data. Choose what to measure, group it, and decide how it should look.</p>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
{editing && (
|
|
236
|
+
<section className="cms-dashboard-builder">
|
|
237
|
+
<div className="cms-dashboard-builder-head">
|
|
238
|
+
<div>
|
|
239
|
+
<p className="section-label">Dashboard builder</p>
|
|
240
|
+
<h3>Configure your widgets</h3>
|
|
241
|
+
<p>Each widget is a small question about your content. Add a widget, choose its source and measure, then save.</p>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="cms-dashboard-builder-actions">
|
|
244
|
+
<Button variant="positive" type="button" onClick={addWidget}>+ Add widget</Button>
|
|
245
|
+
<Button variant="primary" type="button" disabled={saving} onClick={() => void save()}>{saving ? 'Saving…' : 'Save dashboard'}</Button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div className="cms-dashboard-config-list">
|
|
249
|
+
{config.widgets.map((widget, index) => (
|
|
250
|
+
<WidgetConfig
|
|
251
|
+
key={widget.id}
|
|
252
|
+
widget={widget}
|
|
253
|
+
dashboardSources={dashboardSources}
|
|
254
|
+
onChange={(patch) => updateWidget(widget.id, patch)}
|
|
255
|
+
onRemove={() => removeWidget(widget.id)}
|
|
256
|
+
onMove={(direction) => moveWidget(widget.id, direction)}
|
|
257
|
+
canMoveUp={index > 0}
|
|
258
|
+
canMoveDown={index < config.widgets.length - 1}
|
|
259
|
+
/>
|
|
260
|
+
))}
|
|
261
|
+
{!config.widgets.length && <p className="cms-empty-state">No widgets yet. Add your first widget above.</p>}
|
|
262
|
+
</div>
|
|
263
|
+
</section>
|
|
264
|
+
)}
|
|
265
|
+
<section className="cms-dashboard-preview">
|
|
266
|
+
<div className="cms-dashboard-preview-head">
|
|
267
|
+
<div>
|
|
268
|
+
<p className="section-label">{editing ? 'Live preview' : 'Dashboard'}</p>
|
|
269
|
+
<h3>{editing ? 'This is how your dashboard will look' : 'Content overview'}</h3>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="cms-dashboard-grid">
|
|
273
|
+
{config.widgets.map((widget) => (
|
|
274
|
+
<div key={widget.id} className={`cms-dashboard-cell cms-dashboard-cell--${widget.size}`}>
|
|
275
|
+
<Widget widget={widget} />
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
{!config.widgets.length && <p className="cms-empty-state">No widgets yet.</p>}
|
|
279
|
+
</div>
|
|
280
|
+
</section>
|
|
281
|
+
</section>
|
|
282
|
+
</main>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function DragHandle() {
|
|
2
|
+
return <svg viewBox="0 0 12 16" aria-hidden="true"><circle cx="3" cy="2" r="1.4" /><circle cx="9" cy="2" r="1.4" /><circle cx="3" cy="8" r="1.4" /><circle cx="9" cy="8" r="1.4" /><circle cx="3" cy="14" r="1.4" /><circle cx="9" cy="14" r="1.4" /></svg>
|
|
3
|
+
}
|