@actuate-media/cms-admin 0.2.1 → 0.4.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/dist/AdminRoot.js +2 -2
- package/dist/AdminRoot.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/ContentOverviewChart.d.ts +8 -0
- package/dist/components/ContentOverviewChart.d.ts.map +1 -0
- package/dist/components/ContentOverviewChart.js +20 -0
- package/dist/components/ContentOverviewChart.js.map +1 -0
- package/dist/components/SEOPerformance.d.ts +5 -0
- package/dist/components/SEOPerformance.d.ts.map +1 -0
- package/dist/components/SEOPerformance.js +24 -0
- package/dist/components/SEOPerformance.js.map +1 -0
- package/dist/layout/Sidebar.d.ts +7 -0
- package/dist/layout/Sidebar.d.ts.map +1 -1
- package/dist/layout/Sidebar.js +34 -10
- package/dist/layout/Sidebar.js.map +1 -1
- package/dist/views/Dashboard.d.ts +2 -1
- package/dist/views/Dashboard.d.ts.map +1 -1
- package/dist/views/Dashboard.js +81 -32
- package/dist/views/Dashboard.js.map +1 -1
- package/dist/views/DocumentEdit.d.ts.map +1 -1
- package/dist/views/DocumentEdit.js +49 -5
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Pages.js +57 -2
- package/dist/views/Pages.js.map +1 -1
- package/dist/views/Settings.d.ts +2 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +31 -3
- package/dist/views/Settings.js.map +1 -1
- package/package.json +3 -2
- package/src/AdminRoot.tsx +2 -2
- package/src/components/ContentOverviewChart.tsx +70 -0
- package/src/components/SEOPerformance.tsx +134 -0
- package/src/layout/Sidebar.tsx +70 -22
- package/src/views/Dashboard.tsx +175 -192
- package/src/views/DocumentEdit.tsx +82 -4
- package/src/views/Pages.tsx +132 -58
- package/src/views/Settings.tsx +79 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, AlertTriangle, FileWarning, ImageOff, LinkIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { useApiData } from '../lib/useApiData.js';
|
|
6
|
+
|
|
7
|
+
interface SEOSummary {
|
|
8
|
+
totalPages: number;
|
|
9
|
+
issuesSummary: {
|
|
10
|
+
missingMetaDescriptions: number;
|
|
11
|
+
brokenInternalLinks: number;
|
|
12
|
+
missingAltText: number;
|
|
13
|
+
};
|
|
14
|
+
topContent: { id: string; title: string; collection: string; score: number }[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ScoreBadge({ score }: { score: number }) {
|
|
18
|
+
const color = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-amber-500' : 'text-red-500';
|
|
19
|
+
const bg = score >= 80 ? 'bg-green-50' : score >= 60 ? 'bg-amber-50' : 'bg-red-50';
|
|
20
|
+
return (
|
|
21
|
+
<span className={`inline-flex items-center justify-center w-9 h-9 rounded-full text-sm font-semibold ${color} ${bg}`}>
|
|
22
|
+
{score}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SEOPerformanceProps {
|
|
28
|
+
onNavigate?: (path: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SEOPerformance({ onNavigate }: SEOPerformanceProps) {
|
|
32
|
+
const { data, loading, error } = useApiData<SEOSummary>('/seo/summary');
|
|
33
|
+
const [page, setPage] = useState(0);
|
|
34
|
+
const perPage = 5;
|
|
35
|
+
|
|
36
|
+
if (loading || error || !data) return null;
|
|
37
|
+
|
|
38
|
+
const issues = data.issuesSummary;
|
|
39
|
+
const totalIssues = issues.missingMetaDescriptions + issues.brokenInternalLinks + issues.missingAltText;
|
|
40
|
+
const topContent = data.topContent ?? [];
|
|
41
|
+
const totalPages = Math.max(1, Math.ceil(topContent.length / perPage));
|
|
42
|
+
const visible = topContent.slice(page * perPage, (page + 1) * perPage);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="bg-white rounded-lg border border-gray-200">
|
|
46
|
+
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
47
|
+
<div className="flex items-center gap-2">
|
|
48
|
+
<Search className="w-4 h-4 text-gray-500" />
|
|
49
|
+
<h2 className="text-sm font-semibold text-gray-900">SEO Performance</h2>
|
|
50
|
+
</div>
|
|
51
|
+
{totalIssues > 0 && (
|
|
52
|
+
<span className="text-xs text-gray-500">{totalIssues} issue{totalIssues !== 1 ? 's' : ''} found</span>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x divide-gray-200">
|
|
57
|
+
<div className="lg:col-span-7 p-4">
|
|
58
|
+
<h3 className="text-sm font-medium text-gray-700 mb-3">Top Performing Content</h3>
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
{visible.map((item) => (
|
|
61
|
+
<div key={item.id} className="flex items-center justify-between py-2">
|
|
62
|
+
<div className="flex-1 min-w-0 mr-3">
|
|
63
|
+
<p className="text-sm font-medium text-gray-900 truncate">{item.title}</p>
|
|
64
|
+
<p className="text-xs text-gray-500 capitalize">{item.collection}</p>
|
|
65
|
+
</div>
|
|
66
|
+
<ScoreBadge score={item.score} />
|
|
67
|
+
</div>
|
|
68
|
+
))}
|
|
69
|
+
{visible.length === 0 && (
|
|
70
|
+
<p className="text-sm text-gray-400 py-4 text-center">No published content yet</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
{topContent.length > perPage && (
|
|
74
|
+
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100 text-xs text-gray-500">
|
|
75
|
+
<span>Showing {page * perPage + 1}-{Math.min((page + 1) * perPage, topContent.length)} of {topContent.length}</span>
|
|
76
|
+
<div className="flex items-center gap-1">
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => setPage(Math.max(0, page - 1))}
|
|
79
|
+
disabled={page === 0}
|
|
80
|
+
className="p-1 rounded hover:bg-gray-100 disabled:opacity-30"
|
|
81
|
+
>
|
|
82
|
+
<ChevronLeft className="w-3.5 h-3.5" />
|
|
83
|
+
</button>
|
|
84
|
+
<span>Page {page + 1} of {totalPages}</span>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setPage(Math.min(totalPages - 1, page + 1))}
|
|
87
|
+
disabled={page >= totalPages - 1}
|
|
88
|
+
className="p-1 rounded hover:bg-gray-100 disabled:opacity-30"
|
|
89
|
+
>
|
|
90
|
+
<ChevronRight className="w-3.5 h-3.5" />
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="lg:col-span-5 p-4">
|
|
98
|
+
<h3 className="text-sm font-medium text-gray-700 mb-3">SEO Issues</h3>
|
|
99
|
+
<div className="space-y-3">
|
|
100
|
+
<div className="flex items-center justify-between">
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<FileWarning className="w-4 h-4 text-red-400" />
|
|
103
|
+
<span className="text-sm text-gray-700">Missing meta descriptions</span>
|
|
104
|
+
</div>
|
|
105
|
+
<span className="text-sm font-semibold text-gray-900">{issues.missingMetaDescriptions}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex items-center justify-between">
|
|
108
|
+
<div className="flex items-center gap-2">
|
|
109
|
+
<LinkIcon className="w-4 h-4 text-red-400" />
|
|
110
|
+
<span className="text-sm text-gray-700">Broken internal links</span>
|
|
111
|
+
</div>
|
|
112
|
+
<span className="text-sm font-semibold text-gray-900">{issues.brokenInternalLinks}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex items-center justify-between">
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<ImageOff className="w-4 h-4 text-red-400" />
|
|
117
|
+
<span className="text-sm text-gray-700">Missing alt text</span>
|
|
118
|
+
</div>
|
|
119
|
+
<span className="text-sm font-semibold text-gray-900">{issues.missingAltText}</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
{totalIssues > 0 && (
|
|
123
|
+
<button
|
|
124
|
+
onClick={() => onNavigate?.('/seo')}
|
|
125
|
+
className="mt-4 text-sm text-blue-600 hover:text-blue-700 font-medium"
|
|
126
|
+
>
|
|
127
|
+
View All Issues
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
package/src/layout/Sidebar.tsx
CHANGED
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
BookOpen,
|
|
17
17
|
HelpCircle,
|
|
18
18
|
Newspaper,
|
|
19
|
+
PanelTop,
|
|
20
|
+
PanelBottom,
|
|
21
|
+
Layers,
|
|
19
22
|
} from 'lucide-react';
|
|
20
23
|
import type { LucideIcon } from 'lucide-react';
|
|
21
24
|
|
|
@@ -53,6 +56,9 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
53
56
|
book: BookOpen,
|
|
54
57
|
help: HelpCircle,
|
|
55
58
|
newspaper: Newspaper,
|
|
59
|
+
PanelTop: PanelTop,
|
|
60
|
+
PanelBottom: PanelBottom,
|
|
61
|
+
Layers: Layers,
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
function BrandLogo({ config, collapsed }: { config?: any; collapsed: boolean }) {
|
|
@@ -143,28 +149,44 @@ export function Sidebar({ collapsed, onToggleCollapse, currentPath, onNavigate,
|
|
|
143
149
|
</div>
|
|
144
150
|
|
|
145
151
|
<nav className="p-3 space-y-1">
|
|
146
|
-
{navItems.map((item) => {
|
|
152
|
+
{navItems.map((item, idx) => {
|
|
147
153
|
const Icon = item.icon;
|
|
148
154
|
const isActive =
|
|
149
155
|
currentPath === item.path ||
|
|
150
156
|
(item.path !== '/' && currentPath.startsWith(item.path));
|
|
151
157
|
|
|
158
|
+
const prevGroup = idx > 0 ? navItems[idx - 1]?.group : undefined;
|
|
159
|
+
const showGroupLabel = item.group && item.group !== prevGroup;
|
|
160
|
+
|
|
152
161
|
return (
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<span className="text-sm font-medium">{item.label}</span>
|
|
162
|
+
<div key={item.path}>
|
|
163
|
+
{showGroupLabel && !collapsed && (
|
|
164
|
+
<div className="pt-3 pb-1 px-3">
|
|
165
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-sidebar-foreground/50">
|
|
166
|
+
{item.group}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
{showGroupLabel && collapsed && (
|
|
171
|
+
<div className="pt-2 pb-1 flex justify-center">
|
|
172
|
+
<span className="w-4 border-t border-sidebar-foreground/20" />
|
|
173
|
+
</div>
|
|
166
174
|
)}
|
|
167
|
-
|
|
175
|
+
<button
|
|
176
|
+
onClick={() => onNavigate(item.path)}
|
|
177
|
+
className={`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors w-full text-left ${
|
|
178
|
+
isActive
|
|
179
|
+
? 'bg-sidebar-accent text-sidebar-primary'
|
|
180
|
+
: 'text-sidebar-foreground hover:bg-sidebar-accent'
|
|
181
|
+
} ${collapsed ? 'justify-center' : ''}`}
|
|
182
|
+
title={collapsed ? item.label : ''}
|
|
183
|
+
>
|
|
184
|
+
<Icon className="w-5 h-5 shrink-0" />
|
|
185
|
+
{!collapsed && (
|
|
186
|
+
<span className="text-sm font-medium">{item.label}</span>
|
|
187
|
+
)}
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
168
190
|
);
|
|
169
191
|
})}
|
|
170
192
|
</nav>
|
|
@@ -178,7 +200,14 @@ function resolveIcon(collection: any): LucideIcon {
|
|
|
178
200
|
return collection.type === 'page' ? File : FileText;
|
|
179
201
|
}
|
|
180
202
|
|
|
181
|
-
|
|
203
|
+
export interface NavItem {
|
|
204
|
+
path: string;
|
|
205
|
+
label: string;
|
|
206
|
+
icon: LucideIcon;
|
|
207
|
+
group?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildNavItems(config: any): NavItem[] {
|
|
182
211
|
if (!config?.collections) return defaultNavItems;
|
|
183
212
|
|
|
184
213
|
const raw = config.collections;
|
|
@@ -186,17 +215,19 @@ function buildNavItems(config: any) {
|
|
|
186
215
|
|
|
187
216
|
const visible = collectionsList.filter((c) => !c.admin?.hidden);
|
|
188
217
|
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const other = visible.filter((c) => c.type !== 'page' && c.type !== 'post');
|
|
218
|
+
const ungrouped = visible.filter((c) => !c.admin?.group);
|
|
219
|
+
const grouped = visible.filter((c) => c.admin?.group);
|
|
192
220
|
|
|
193
|
-
const
|
|
221
|
+
const pages = ungrouped.filter((c) => c.type === 'page');
|
|
222
|
+
const posts = ungrouped.filter((c) => c.type === 'post');
|
|
223
|
+
const other = ungrouped.filter((c) => c.type !== 'page' && c.type !== 'post');
|
|
224
|
+
const sortedUngrouped = [...pages, ...posts, ...other];
|
|
194
225
|
|
|
195
|
-
const items:
|
|
226
|
+
const items: NavItem[] = [
|
|
196
227
|
{ path: '/', label: 'Dashboard', icon: LayoutDashboard },
|
|
197
228
|
];
|
|
198
229
|
|
|
199
|
-
for (const collection of
|
|
230
|
+
for (const collection of sortedUngrouped) {
|
|
200
231
|
items.push({
|
|
201
232
|
label: collection.labels?.plural ?? collection.slug,
|
|
202
233
|
path: `/${collection.slug}`,
|
|
@@ -204,6 +235,23 @@ function buildNavItems(config: any) {
|
|
|
204
235
|
});
|
|
205
236
|
}
|
|
206
237
|
|
|
238
|
+
const groups = new Map<string, typeof grouped>();
|
|
239
|
+
for (const col of grouped) {
|
|
240
|
+
const group = col.admin.group as string;
|
|
241
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
242
|
+
groups.get(group)!.push(col);
|
|
243
|
+
}
|
|
244
|
+
for (const [groupName, cols] of groups) {
|
|
245
|
+
for (const collection of cols) {
|
|
246
|
+
items.push({
|
|
247
|
+
label: collection.labels?.plural ?? collection.slug,
|
|
248
|
+
path: `/${collection.slug}`,
|
|
249
|
+
icon: resolveIcon(collection),
|
|
250
|
+
group: groupName,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
207
255
|
items.push(
|
|
208
256
|
{ path: '/media', label: 'Media', icon: Image },
|
|
209
257
|
{ path: '/forms', label: 'Forms', icon: ClipboardList },
|