@orion-studios/payload-admin-components 0.1.0 → 0.2.0-beta.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/admin.css +68 -57
- package/dist/index.d.mts +119 -1
- package/dist/index.d.ts +119 -1
- package/dist/index.js +1233 -76
- package/dist/index.mjs +1220 -75
- package/dist/styles/admin.css +85 -0
- package/dist/styles/overrides.css +375 -0
- package/dist/styles/themes/brand-dark.css +69 -0
- package/dist/styles/themes/brand-light.css +69 -0
- package/dist/styles/themes/dark.css +68 -0
- package/dist/styles/themes/light.css +68 -0
- package/package.json +3 -3
- package/src/components/BlockPicker.tsx +167 -0
- package/src/components/Dashboard.tsx +415 -0
- package/src/components/EmptyState.tsx +86 -0
- package/src/components/HelpTooltip.tsx +121 -0
- package/src/components/Icon.tsx +16 -0
- package/src/components/Logo.tsx +52 -0
- package/src/components/SectionTabs.tsx +84 -0
- package/src/components/StatusBadge.tsx +49 -0
- package/src/components/ThemeSwitcher.tsx +120 -0
- package/src/components/WelcomeHeader.tsx +54 -0
- package/src/fields/themePreference.ts +22 -0
- package/src/helpers/configureAdmin.ts +122 -0
- package/src/helpers/withTooltips.ts +91 -0
- package/src/hooks/useTheme.ts +128 -0
- package/src/index.ts +27 -0
- package/src/styles/admin.css +68 -57
- package/src/styles/overrides.css +375 -0
- package/src/styles/themes/brand-dark.css +69 -0
- package/src/styles/themes/brand-light.css +69 -0
- package/src/styles/themes/dark.css +68 -0
- package/src/styles/themes/light.css +68 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* Light Theme - Clean whites, subtle grays, blue accents */
|
|
2
|
+
[data-theme="light"] {
|
|
3
|
+
--admin-bg: #ffffff;
|
|
4
|
+
--admin-surface: #f9fafb;
|
|
5
|
+
--admin-surface-elevated: #ffffff;
|
|
6
|
+
--admin-border: #e5e7eb;
|
|
7
|
+
--admin-border-subtle: #f3f4f6;
|
|
8
|
+
|
|
9
|
+
--admin-text: #111827;
|
|
10
|
+
--admin-text-secondary: #4b5563;
|
|
11
|
+
--admin-text-muted: #9ca3af;
|
|
12
|
+
--admin-text-inverse: #ffffff;
|
|
13
|
+
|
|
14
|
+
--admin-accent: #3b82f6;
|
|
15
|
+
--admin-accent-hover: #2563eb;
|
|
16
|
+
--admin-accent-subtle: #eff6ff;
|
|
17
|
+
--admin-accent-secondary: #8b5cf6;
|
|
18
|
+
--admin-accent-secondary-hover: #7c3aed;
|
|
19
|
+
--admin-accent-secondary-subtle: #f5f3ff;
|
|
20
|
+
|
|
21
|
+
--admin-nav-bg: #f8fafc;
|
|
22
|
+
--admin-nav-text: #374151;
|
|
23
|
+
--admin-nav-text-active: #111827;
|
|
24
|
+
--admin-nav-item-hover: #f1f5f9;
|
|
25
|
+
--admin-nav-item-active: #e0e7ff;
|
|
26
|
+
--admin-nav-group-text: #6b7280;
|
|
27
|
+
--admin-nav-border: #e2e8f0;
|
|
28
|
+
|
|
29
|
+
--admin-input-bg: #ffffff;
|
|
30
|
+
--admin-input-border: #d1d5db;
|
|
31
|
+
--admin-input-border-focus: var(--admin-accent);
|
|
32
|
+
--admin-input-placeholder: #9ca3af;
|
|
33
|
+
|
|
34
|
+
--admin-card-bg: #ffffff;
|
|
35
|
+
--admin-card-border: #e5e7eb;
|
|
36
|
+
--admin-card-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
37
|
+
--admin-card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
|
|
38
|
+
|
|
39
|
+
--admin-success: #16a34a;
|
|
40
|
+
--admin-success-bg: #f0fdf4;
|
|
41
|
+
--admin-warning: #d97706;
|
|
42
|
+
--admin-warning-bg: #fffbeb;
|
|
43
|
+
--admin-error: #dc2626;
|
|
44
|
+
--admin-error-bg: #fef2f2;
|
|
45
|
+
--admin-info: #0284c7;
|
|
46
|
+
--admin-info-bg: #f0f9ff;
|
|
47
|
+
|
|
48
|
+
--admin-badge-draft-bg: #fef3c7;
|
|
49
|
+
--admin-badge-draft-text: #92400e;
|
|
50
|
+
--admin-badge-published-bg: #dcfce7;
|
|
51
|
+
--admin-badge-published-text: #166534;
|
|
52
|
+
--admin-badge-changed-bg: #dbeafe;
|
|
53
|
+
--admin-badge-changed-text: #1e40af;
|
|
54
|
+
|
|
55
|
+
--admin-tooltip-bg: #1f2937;
|
|
56
|
+
--admin-tooltip-text: #f9fafb;
|
|
57
|
+
|
|
58
|
+
--admin-overlay: rgba(0, 0, 0, 0.3);
|
|
59
|
+
--admin-scrollbar-track: #f1f5f9;
|
|
60
|
+
--admin-scrollbar-thumb: #cbd5e1;
|
|
61
|
+
--admin-scrollbar-thumb-hover: #94a3b8;
|
|
62
|
+
|
|
63
|
+
--admin-focus-ring: 0 0 0 2px var(--admin-accent-subtle), 0 0 0 4px var(--admin-accent);
|
|
64
|
+
--admin-radius-sm: 6px;
|
|
65
|
+
--admin-radius-md: 8px;
|
|
66
|
+
--admin-radius-lg: 12px;
|
|
67
|
+
--admin-radius-xl: 16px;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orion-studios/payload-admin-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
4
|
"description": "Custom admin UI components for Payload CMS",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"src
|
|
18
|
+
"src"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "tsup src/index.ts --format cjs,esm --dts &&
|
|
21
|
+
"build": "tsup src/index.ts --format cjs,esm --dts && node -e \"const fs=require('fs'),path=require('path');function copyDir(s,d){fs.mkdirSync(d,{recursive:true});for(const f of fs.readdirSync(s)){const sp=path.join(s,f),dp=path.join(d,f);fs.statSync(sp).isDirectory()?copyDir(sp,dp):fs.copyFileSync(sp,dp)}}copyDir('src/styles','dist/styles');fs.copyFileSync('src/styles/admin.css','dist/admin.css')\"",
|
|
22
22
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
23
23
|
"typecheck": "tsc --noEmit"
|
|
24
24
|
},
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface BlockOption {
|
|
6
|
+
slug: string
|
|
7
|
+
label: string
|
|
8
|
+
description?: string
|
|
9
|
+
icon?: React.ReactNode
|
|
10
|
+
imageURL?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Visual card-based block picker. Use as a replacement for Payload's default
|
|
15
|
+
* dropdown block selector. Shows blocks as a grid of cards with icons and descriptions.
|
|
16
|
+
*/
|
|
17
|
+
export function BlockPicker({
|
|
18
|
+
blocks,
|
|
19
|
+
onSelect,
|
|
20
|
+
}: {
|
|
21
|
+
blocks: BlockOption[]
|
|
22
|
+
onSelect: (slug: string) => void
|
|
23
|
+
}) {
|
|
24
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
25
|
+
|
|
26
|
+
const filtered = blocks.filter(
|
|
27
|
+
(b) =>
|
|
28
|
+
b.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
29
|
+
b.slug.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
30
|
+
(b.description && b.description.toLowerCase().includes(searchQuery.toLowerCase())),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
35
|
+
{/* Search */}
|
|
36
|
+
{blocks.length > 4 && (
|
|
37
|
+
<input
|
|
38
|
+
type="text"
|
|
39
|
+
placeholder="Search sections..."
|
|
40
|
+
value={searchQuery}
|
|
41
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
42
|
+
style={{
|
|
43
|
+
padding: '10px 14px',
|
|
44
|
+
border: '1px solid var(--admin-input-border)',
|
|
45
|
+
borderRadius: 'var(--admin-radius-md)',
|
|
46
|
+
background: 'var(--admin-input-bg)',
|
|
47
|
+
color: 'var(--admin-text)',
|
|
48
|
+
fontSize: 14,
|
|
49
|
+
outline: 'none',
|
|
50
|
+
width: '100%',
|
|
51
|
+
boxSizing: 'border-box',
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Block Grid */}
|
|
57
|
+
<div
|
|
58
|
+
style={{
|
|
59
|
+
display: 'grid',
|
|
60
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
|
61
|
+
gap: 12,
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{filtered.map((block) => (
|
|
65
|
+
<button
|
|
66
|
+
key={block.slug}
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => onSelect(block.slug)}
|
|
69
|
+
style={{
|
|
70
|
+
display: 'flex',
|
|
71
|
+
flexDirection: 'column',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: 10,
|
|
74
|
+
padding: 16,
|
|
75
|
+
background: 'var(--admin-card-bg)',
|
|
76
|
+
border: '1px solid var(--admin-card-border)',
|
|
77
|
+
borderRadius: 'var(--admin-radius-lg)',
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
transition: 'all 0.2s ease',
|
|
80
|
+
textAlign: 'center',
|
|
81
|
+
}}
|
|
82
|
+
onMouseEnter={(e) => {
|
|
83
|
+
e.currentTarget.style.borderColor = 'var(--admin-accent)'
|
|
84
|
+
e.currentTarget.style.boxShadow = 'var(--admin-card-shadow-hover)'
|
|
85
|
+
e.currentTarget.style.transform = 'translateY(-2px)'
|
|
86
|
+
}}
|
|
87
|
+
onMouseLeave={(e) => {
|
|
88
|
+
e.currentTarget.style.borderColor = 'var(--admin-card-border)'
|
|
89
|
+
e.currentTarget.style.boxShadow = 'none'
|
|
90
|
+
e.currentTarget.style.transform = 'translateY(0)'
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{/* Icon or image */}
|
|
94
|
+
<div
|
|
95
|
+
style={{
|
|
96
|
+
width: 48,
|
|
97
|
+
height: 48,
|
|
98
|
+
borderRadius: 'var(--admin-radius-md)',
|
|
99
|
+
background: 'var(--admin-accent-subtle)',
|
|
100
|
+
color: 'var(--admin-accent)',
|
|
101
|
+
display: 'flex',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
fontSize: 24,
|
|
105
|
+
overflow: 'hidden',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{block.imageURL ? (
|
|
109
|
+
<img
|
|
110
|
+
src={block.imageURL}
|
|
111
|
+
alt={block.label}
|
|
112
|
+
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
113
|
+
/>
|
|
114
|
+
) : block.icon ? (
|
|
115
|
+
block.icon
|
|
116
|
+
) : (
|
|
117
|
+
<BlockDefaultIcon />
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Label */}
|
|
122
|
+
<span
|
|
123
|
+
style={{
|
|
124
|
+
fontSize: 13,
|
|
125
|
+
fontWeight: 600,
|
|
126
|
+
color: 'var(--admin-text)',
|
|
127
|
+
lineHeight: 1.3,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{block.label}
|
|
131
|
+
</span>
|
|
132
|
+
|
|
133
|
+
{/* Description */}
|
|
134
|
+
{block.description && (
|
|
135
|
+
<span
|
|
136
|
+
style={{
|
|
137
|
+
fontSize: 11,
|
|
138
|
+
color: 'var(--admin-text-muted)',
|
|
139
|
+
lineHeight: 1.4,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{block.description}
|
|
143
|
+
</span>
|
|
144
|
+
)}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{filtered.length === 0 && (
|
|
150
|
+
<p style={{ textAlign: 'center', color: 'var(--admin-text-muted)', fontSize: 14, padding: 20 }}>
|
|
151
|
+
No sections match your search.
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function BlockDefaultIcon() {
|
|
159
|
+
return (
|
|
160
|
+
<svg width={24} height={24} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
161
|
+
<rect x="3" y="3" width="7" height="7" />
|
|
162
|
+
<rect x="14" y="3" width="7" height="7" />
|
|
163
|
+
<rect x="14" y="14" width="7" height="7" />
|
|
164
|
+
<rect x="3" y="14" width="7" height="7" />
|
|
165
|
+
</svg>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { ThemeSwitcher } from './ThemeSwitcher'
|
|
5
|
+
import { HelpTooltip } from './HelpTooltip'
|
|
6
|
+
import { StatusBadge } from './StatusBadge'
|
|
7
|
+
|
|
8
|
+
// SVG Icons as components
|
|
9
|
+
function PagesIcon({ size = 24 }: { size?: number }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
12
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
13
|
+
<polyline points="14 2 14 8 20 8" />
|
|
14
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
15
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
16
|
+
<polyline points="10 9 9 9 8 9" />
|
|
17
|
+
</svg>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function MediaIcon({ size = 24 }: { size?: number }) {
|
|
22
|
+
return (
|
|
23
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
24
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
25
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
26
|
+
<polyline points="21 15 16 10 5 21" />
|
|
27
|
+
</svg>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function SettingsIcon({ size = 24 }: { size?: number }) {
|
|
32
|
+
return (
|
|
33
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
34
|
+
<circle cx="12" cy="12" r="3" />
|
|
35
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
36
|
+
</svg>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function LayoutIcon({ size = 24 }: { size?: number }) {
|
|
41
|
+
return (
|
|
42
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round">
|
|
43
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
44
|
+
<line x1="3" y1="9" x2="21" y2="9" />
|
|
45
|
+
<line x1="3" y1="15" x2="21" y2="15" />
|
|
46
|
+
</svg>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function PlusIcon({ size = 16 }: { size?: number }) {
|
|
51
|
+
return (
|
|
52
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
53
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
54
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
55
|
+
</svg>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ClockIcon({ size = 14 }: { size?: number }) {
|
|
60
|
+
return (
|
|
61
|
+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
62
|
+
<circle cx="12" cy="12" r="10" />
|
|
63
|
+
<polyline points="12 6 12 12 16 14" />
|
|
64
|
+
</svg>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getGreeting(): string {
|
|
69
|
+
const hour = new Date().getHours()
|
|
70
|
+
if (hour < 12) return 'Good morning'
|
|
71
|
+
if (hour < 17) return 'Good afternoon'
|
|
72
|
+
return 'Good evening'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatRelativeTime(dateStr: string): string {
|
|
76
|
+
const date = new Date(dateStr)
|
|
77
|
+
const now = new Date()
|
|
78
|
+
const diffMs = now.getTime() - date.getTime()
|
|
79
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
80
|
+
const diffHours = Math.floor(diffMs / 3600000)
|
|
81
|
+
const diffDays = Math.floor(diffMs / 86400000)
|
|
82
|
+
|
|
83
|
+
if (diffMins < 1) return 'Just now'
|
|
84
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
85
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
86
|
+
if (diffDays < 7) return `${diffDays}d ago`
|
|
87
|
+
return date.toLocaleDateString()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface RecentItem {
|
|
91
|
+
id: string
|
|
92
|
+
title?: string
|
|
93
|
+
slug?: string
|
|
94
|
+
_status?: string
|
|
95
|
+
updatedAt: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function Dashboard() {
|
|
99
|
+
const [userName, setUserName] = useState('')
|
|
100
|
+
const [recentPages, setRecentPages] = useState<RecentItem[]>([])
|
|
101
|
+
const [pageCount, setPageCount] = useState<number | null>(null)
|
|
102
|
+
const [mediaCount, setMediaCount] = useState<number | null>(null)
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
// Fetch current user
|
|
106
|
+
fetch('/api/users/me', { credentials: 'include' })
|
|
107
|
+
.then((res) => res.json())
|
|
108
|
+
.then((data) => {
|
|
109
|
+
const user = data?.user || data
|
|
110
|
+
setUserName(user?.fullName || user?.email?.split('@')[0] || '')
|
|
111
|
+
})
|
|
112
|
+
.catch(() => {})
|
|
113
|
+
|
|
114
|
+
// Fetch recent pages
|
|
115
|
+
fetch('/api/pages?limit=5&sort=-updatedAt', { credentials: 'include' })
|
|
116
|
+
.then((res) => res.json())
|
|
117
|
+
.then((data) => {
|
|
118
|
+
setRecentPages(data?.docs || [])
|
|
119
|
+
setPageCount(data?.totalDocs ?? null)
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {})
|
|
122
|
+
|
|
123
|
+
// Fetch media count
|
|
124
|
+
fetch('/api/media?limit=0', { credentials: 'include' })
|
|
125
|
+
.then((res) => res.json())
|
|
126
|
+
.then((data) => {
|
|
127
|
+
setMediaCount(data?.totalDocs ?? null)
|
|
128
|
+
})
|
|
129
|
+
.catch(() => {})
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div style={{ padding: '32px', maxWidth: 1200, margin: '0 auto' }}>
|
|
134
|
+
{/* Welcome Header */}
|
|
135
|
+
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 32 }}>
|
|
136
|
+
<div>
|
|
137
|
+
<h1 style={{ fontSize: 28, fontWeight: 700, color: 'var(--admin-text)', margin: '0 0 6px' }}>
|
|
138
|
+
{getGreeting()}{userName ? `, ${userName}` : ''}
|
|
139
|
+
</h1>
|
|
140
|
+
<p style={{ fontSize: 15, color: 'var(--admin-text-muted)', margin: 0 }}>
|
|
141
|
+
Manage your website content and settings from here.
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
<ThemeSwitcher />
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Quick Actions */}
|
|
148
|
+
<div style={{ display: 'flex', gap: 10, marginBottom: 32, flexWrap: 'wrap' }}>
|
|
149
|
+
<QuickAction href="/admin/collections/pages/create" icon={<PlusIcon />} label="New Page" />
|
|
150
|
+
<QuickAction href="/admin/collections/media/create" icon={<PlusIcon />} label="Upload Media" />
|
|
151
|
+
<QuickAction href="/admin/globals/header" icon={<LayoutIcon size={16} />} label="Edit Navigation" />
|
|
152
|
+
<QuickAction href="/admin/globals/site-settings" icon={<SettingsIcon size={16} />} label="Website Settings" />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Content Cards Grid */}
|
|
156
|
+
<div
|
|
157
|
+
style={{
|
|
158
|
+
display: 'grid',
|
|
159
|
+
gap: 20,
|
|
160
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
|
161
|
+
marginBottom: 32,
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
{/* Pages Card */}
|
|
165
|
+
<ContentCard
|
|
166
|
+
icon={<PagesIcon />}
|
|
167
|
+
title="Pages"
|
|
168
|
+
description="Create and manage your website pages"
|
|
169
|
+
count={pageCount}
|
|
170
|
+
countLabel="pages"
|
|
171
|
+
tooltip="Pages are the individual sections of your website. Each page has a URL and contains content sections."
|
|
172
|
+
actions={[
|
|
173
|
+
{ label: 'View All', href: '/admin/collections/pages' },
|
|
174
|
+
{ label: 'Create New', href: '/admin/collections/pages/create', primary: true },
|
|
175
|
+
]}
|
|
176
|
+
/>
|
|
177
|
+
|
|
178
|
+
{/* Media Card */}
|
|
179
|
+
<ContentCard
|
|
180
|
+
icon={<MediaIcon />}
|
|
181
|
+
title="Media Library"
|
|
182
|
+
description="Upload and organize images and files"
|
|
183
|
+
count={mediaCount}
|
|
184
|
+
countLabel="files"
|
|
185
|
+
tooltip="Your media library stores all images, documents, and files used across your website."
|
|
186
|
+
actions={[
|
|
187
|
+
{ label: 'Browse', href: '/admin/collections/media' },
|
|
188
|
+
{ label: 'Upload', href: '/admin/collections/media/create', primary: true },
|
|
189
|
+
]}
|
|
190
|
+
/>
|
|
191
|
+
|
|
192
|
+
{/* Site Design Card */}
|
|
193
|
+
<ContentCard
|
|
194
|
+
icon={<LayoutIcon />}
|
|
195
|
+
title="Site Design"
|
|
196
|
+
description="Customize your header, footer, and site-wide settings"
|
|
197
|
+
tooltip="These settings apply to every page on your website — your navigation menu, footer information, and global SEO settings."
|
|
198
|
+
actions={[
|
|
199
|
+
{ label: 'Header & Nav', href: '/admin/globals/header' },
|
|
200
|
+
{ label: 'Footer', href: '/admin/globals/footer' },
|
|
201
|
+
{ label: 'Settings', href: '/admin/globals/site-settings', primary: true },
|
|
202
|
+
]}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Recent Activity */}
|
|
207
|
+
{recentPages.length > 0 && (
|
|
208
|
+
<div
|
|
209
|
+
style={{
|
|
210
|
+
background: 'var(--admin-card-bg)',
|
|
211
|
+
border: '1px solid var(--admin-card-border)',
|
|
212
|
+
borderRadius: 'var(--admin-radius-lg)',
|
|
213
|
+
overflow: 'hidden',
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<div
|
|
217
|
+
style={{
|
|
218
|
+
padding: '16px 20px',
|
|
219
|
+
borderBottom: '1px solid var(--admin-border-subtle)',
|
|
220
|
+
display: 'flex',
|
|
221
|
+
alignItems: 'center',
|
|
222
|
+
justifyContent: 'space-between',
|
|
223
|
+
}}
|
|
224
|
+
>
|
|
225
|
+
<h3 style={{ fontSize: 15, fontWeight: 600, color: 'var(--admin-text)', margin: 0, display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
226
|
+
<ClockIcon /> Recently Edited
|
|
227
|
+
</h3>
|
|
228
|
+
<a
|
|
229
|
+
href="/admin/collections/pages"
|
|
230
|
+
style={{ fontSize: 13, color: 'var(--admin-accent)', textDecoration: 'none', fontWeight: 500 }}
|
|
231
|
+
>
|
|
232
|
+
View all
|
|
233
|
+
</a>
|
|
234
|
+
</div>
|
|
235
|
+
{recentPages.map((page, i) => (
|
|
236
|
+
<a
|
|
237
|
+
key={page.id}
|
|
238
|
+
href={`/admin/collections/pages/${page.id}`}
|
|
239
|
+
style={{
|
|
240
|
+
display: 'flex',
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
justifyContent: 'space-between',
|
|
243
|
+
padding: '12px 20px',
|
|
244
|
+
textDecoration: 'none',
|
|
245
|
+
borderBottom: i < recentPages.length - 1 ? '1px solid var(--admin-border-subtle)' : 'none',
|
|
246
|
+
transition: 'background-color 0.15s ease',
|
|
247
|
+
}}
|
|
248
|
+
onMouseEnter={(e) => {
|
|
249
|
+
e.currentTarget.style.backgroundColor = 'var(--admin-surface)'
|
|
250
|
+
}}
|
|
251
|
+
onMouseLeave={(e) => {
|
|
252
|
+
e.currentTarget.style.backgroundColor = 'transparent'
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
|
|
256
|
+
<PagesIcon size={16} />
|
|
257
|
+
<span style={{ fontSize: 14, fontWeight: 500, color: 'var(--admin-text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
258
|
+
{page.title || page.slug || 'Untitled'}
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
|
|
262
|
+
{page._status && (
|
|
263
|
+
<StatusBadge status={page._status as 'draft' | 'published' | 'changed'} size="sm" />
|
|
264
|
+
)}
|
|
265
|
+
<span style={{ fontSize: 12, color: 'var(--admin-text-muted)', whiteSpace: 'nowrap' }}>
|
|
266
|
+
{formatRelativeTime(page.updatedAt)}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
</a>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function QuickAction({ href, icon, label }: { href: string; icon: React.ReactNode; label: string }) {
|
|
278
|
+
return (
|
|
279
|
+
<a
|
|
280
|
+
href={href}
|
|
281
|
+
style={{
|
|
282
|
+
display: 'inline-flex',
|
|
283
|
+
alignItems: 'center',
|
|
284
|
+
gap: 6,
|
|
285
|
+
padding: '8px 16px',
|
|
286
|
+
background: 'var(--admin-surface)',
|
|
287
|
+
border: '1px solid var(--admin-border)',
|
|
288
|
+
borderRadius: 'var(--admin-radius-md)',
|
|
289
|
+
color: 'var(--admin-text)',
|
|
290
|
+
textDecoration: 'none',
|
|
291
|
+
fontSize: 13,
|
|
292
|
+
fontWeight: 500,
|
|
293
|
+
transition: 'all 0.2s ease',
|
|
294
|
+
whiteSpace: 'nowrap',
|
|
295
|
+
}}
|
|
296
|
+
onMouseEnter={(e) => {
|
|
297
|
+
e.currentTarget.style.borderColor = 'var(--admin-accent)'
|
|
298
|
+
e.currentTarget.style.color = 'var(--admin-accent)'
|
|
299
|
+
e.currentTarget.style.background = 'var(--admin-accent-subtle)'
|
|
300
|
+
}}
|
|
301
|
+
onMouseLeave={(e) => {
|
|
302
|
+
e.currentTarget.style.borderColor = 'var(--admin-border)'
|
|
303
|
+
e.currentTarget.style.color = 'var(--admin-text)'
|
|
304
|
+
e.currentTarget.style.background = 'var(--admin-surface)'
|
|
305
|
+
}}
|
|
306
|
+
>
|
|
307
|
+
{icon}
|
|
308
|
+
{label}
|
|
309
|
+
</a>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ContentCard({
|
|
314
|
+
icon,
|
|
315
|
+
title,
|
|
316
|
+
description,
|
|
317
|
+
count,
|
|
318
|
+
countLabel,
|
|
319
|
+
tooltip,
|
|
320
|
+
actions,
|
|
321
|
+
}: {
|
|
322
|
+
icon: React.ReactNode
|
|
323
|
+
title: string
|
|
324
|
+
description: string
|
|
325
|
+
count?: number | null
|
|
326
|
+
countLabel?: string
|
|
327
|
+
tooltip?: string
|
|
328
|
+
actions?: Array<{ label: string; href: string; primary?: boolean }>
|
|
329
|
+
}) {
|
|
330
|
+
return (
|
|
331
|
+
<div
|
|
332
|
+
style={{
|
|
333
|
+
background: 'var(--admin-card-bg)',
|
|
334
|
+
border: '1px solid var(--admin-card-border)',
|
|
335
|
+
borderRadius: 'var(--admin-radius-lg)',
|
|
336
|
+
padding: 24,
|
|
337
|
+
display: 'flex',
|
|
338
|
+
flexDirection: 'column',
|
|
339
|
+
gap: 16,
|
|
340
|
+
boxShadow: 'var(--admin-card-shadow)',
|
|
341
|
+
transition: 'all 0.2s ease',
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
{/* Card Header */}
|
|
345
|
+
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
|
346
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
347
|
+
<div
|
|
348
|
+
style={{
|
|
349
|
+
width: 40,
|
|
350
|
+
height: 40,
|
|
351
|
+
borderRadius: 'var(--admin-radius-md)',
|
|
352
|
+
background: 'var(--admin-accent-subtle)',
|
|
353
|
+
color: 'var(--admin-accent)',
|
|
354
|
+
display: 'flex',
|
|
355
|
+
alignItems: 'center',
|
|
356
|
+
justifyContent: 'center',
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
{icon}
|
|
360
|
+
</div>
|
|
361
|
+
<div>
|
|
362
|
+
<h3 style={{ fontSize: 16, fontWeight: 600, color: 'var(--admin-text)', margin: 0, display: 'flex', alignItems: 'center' }}>
|
|
363
|
+
{title}
|
|
364
|
+
{tooltip && <HelpTooltip content={tooltip} position="right" />}
|
|
365
|
+
</h3>
|
|
366
|
+
{count !== undefined && count !== null && (
|
|
367
|
+
<span style={{ fontSize: 12, color: 'var(--admin-text-muted)' }}>
|
|
368
|
+
{count} {countLabel}
|
|
369
|
+
</span>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{/* Description */}
|
|
376
|
+
<p style={{ fontSize: 13, color: 'var(--admin-text-muted)', margin: 0, lineHeight: 1.5 }}>
|
|
377
|
+
{description}
|
|
378
|
+
</p>
|
|
379
|
+
|
|
380
|
+
{/* Actions */}
|
|
381
|
+
{actions && actions.length > 0 && (
|
|
382
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', flexWrap: 'wrap' }}>
|
|
383
|
+
{actions.map((action) => (
|
|
384
|
+
<a
|
|
385
|
+
key={action.href}
|
|
386
|
+
href={action.href}
|
|
387
|
+
style={{
|
|
388
|
+
display: 'inline-flex',
|
|
389
|
+
alignItems: 'center',
|
|
390
|
+
padding: '7px 14px',
|
|
391
|
+
borderRadius: 'var(--admin-radius-sm)',
|
|
392
|
+
fontSize: 13,
|
|
393
|
+
fontWeight: 500,
|
|
394
|
+
textDecoration: 'none',
|
|
395
|
+
transition: 'all 0.15s ease',
|
|
396
|
+
...(action.primary
|
|
397
|
+
? {
|
|
398
|
+
background: 'var(--admin-accent)',
|
|
399
|
+
color: 'var(--admin-text-inverse)',
|
|
400
|
+
}
|
|
401
|
+
: {
|
|
402
|
+
background: 'var(--admin-surface)',
|
|
403
|
+
color: 'var(--admin-text-secondary)',
|
|
404
|
+
border: '1px solid var(--admin-border)',
|
|
405
|
+
}),
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{action.label}
|
|
409
|
+
</a>
|
|
410
|
+
))}
|
|
411
|
+
</div>
|
|
412
|
+
)}
|
|
413
|
+
</div>
|
|
414
|
+
)
|
|
415
|
+
}
|