@wikex/admin-kit 0.2.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/CHANGELOG.md +17 -0
- package/README.md +167 -0
- package/RELEASING.md +59 -0
- package/package.json +55 -0
- package/src/TextStyleRuntime.tsx +90 -0
- package/src/admin/BeforeLogin.tsx +19 -0
- package/src/admin/Brand.tsx +74 -0
- package/src/admin/Header.tsx +87 -0
- package/src/admin/LogoutButton.tsx +41 -0
- package/src/admin/Nav.tsx +35 -0
- package/src/admin/NavClient.tsx +288 -0
- package/src/admin/ThemeToggle.tsx +31 -0
- package/src/admin/ViewSite.tsx +11 -0
- package/src/admin/useAdminBrand.ts +81 -0
- package/src/adminExperience.ts +117 -0
- package/src/index.ts +11 -0
- package/src/live-preview/GlobalInlineInspector.tsx +334 -0
- package/src/live-preview/LivePreviewEditor.tsx +2088 -0
- package/src/live-preview/TextStyleInspector.tsx +289 -0
- package/src/live-preview/index.ts +23 -0
- package/src/live-preview/runtimeConfig.ts +52 -0
- package/src/plugin.ts +70 -0
- package/src/project.ts +82 -0
- package/src/rich-text/client.tsx +202 -0
- package/src/rich-text/index.ts +10 -0
- package/src/starter/client.tsx +22 -0
- package/src/starter.ts +95 -0
- package/src/styles/_wikex-fonts.scss +45 -0
- package/src/styles/admin.scss +3032 -0
- package/src/textStyles.ts +161 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Link, useAuth, useConfig, useNav } from '@payloadcms/ui'
|
|
4
|
+
import {
|
|
5
|
+
BookOpenText,
|
|
6
|
+
BriefcaseBusiness,
|
|
7
|
+
ChevronLeft,
|
|
8
|
+
ChevronRight,
|
|
9
|
+
FileText,
|
|
10
|
+
FolderKanban,
|
|
11
|
+
GalleryVerticalEnd,
|
|
12
|
+
Home,
|
|
13
|
+
Image,
|
|
14
|
+
LayoutDashboard,
|
|
15
|
+
Library,
|
|
16
|
+
Menu,
|
|
17
|
+
PanelBottom,
|
|
18
|
+
PanelTop,
|
|
19
|
+
Settings2,
|
|
20
|
+
Tags,
|
|
21
|
+
Users,
|
|
22
|
+
X,
|
|
23
|
+
} from 'lucide-react'
|
|
24
|
+
import type { LucideIcon } from 'lucide-react'
|
|
25
|
+
import { usePathname } from 'next/navigation'
|
|
26
|
+
import { formatAdminURL } from 'payload/shared'
|
|
27
|
+
import React, { useEffect, useMemo, useState } from 'react'
|
|
28
|
+
|
|
29
|
+
import { AdminIcon, AdminLogo } from './Brand'
|
|
30
|
+
import { useAdminBrand } from './useAdminBrand'
|
|
31
|
+
import AdminLogoutButton from './LogoutButton'
|
|
32
|
+
|
|
33
|
+
export type AdminNavGroup = {
|
|
34
|
+
items: Array<{
|
|
35
|
+
label: string
|
|
36
|
+
slug: string
|
|
37
|
+
type: 'collections' | 'globals'
|
|
38
|
+
}>
|
|
39
|
+
label: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type AdminNavClientProps = {
|
|
43
|
+
groups: AdminNavGroup[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type AdminUser = {
|
|
47
|
+
email?: string
|
|
48
|
+
name?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const iconBySlug: Record<string, LucideIcon> = {
|
|
52
|
+
categories: Tags,
|
|
53
|
+
footer: PanelBottom,
|
|
54
|
+
header: PanelTop,
|
|
55
|
+
homepage: Home,
|
|
56
|
+
media: Image,
|
|
57
|
+
pages: FileText,
|
|
58
|
+
posts: BookOpenText,
|
|
59
|
+
projects: FolderKanban,
|
|
60
|
+
services: BriefcaseBusiness,
|
|
61
|
+
settings: Settings2,
|
|
62
|
+
users: Users,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const translateGroupLabel = (label: string) =>
|
|
66
|
+
label === 'Collections' ? 'Bộ sưu tập' : label === 'Globals' ? 'Thiết lập chung' : label
|
|
67
|
+
|
|
68
|
+
const getInitials = (name?: string, email?: string) => {
|
|
69
|
+
const source = name?.trim() || email?.split('@')[0] || 'AD'
|
|
70
|
+
|
|
71
|
+
return source
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.slice(0, 2)
|
|
74
|
+
.map((part) => part.charAt(0).toUpperCase())
|
|
75
|
+
.join('')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const AdminNavClient = ({ groups }: AdminNavClientProps) => {
|
|
79
|
+
const pathname = usePathname()
|
|
80
|
+
const brand = useAdminBrand()
|
|
81
|
+
const { user } = useAuth<AdminUser>()
|
|
82
|
+
const { hydrated, navOpen, navRef, setNavOpen, shouldAnimate } = useNav()
|
|
83
|
+
const {
|
|
84
|
+
config: {
|
|
85
|
+
admin: { routes: adminRoutes },
|
|
86
|
+
folders,
|
|
87
|
+
routes: { admin: adminRoute },
|
|
88
|
+
},
|
|
89
|
+
} = useConfig()
|
|
90
|
+
const [isMini, setIsMini] = useState(false)
|
|
91
|
+
const [isMobile, setIsMobile] = useState(false)
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const storedValue = window.localStorage.getItem('minimal-admin-nav-mini')
|
|
95
|
+
setIsMini(storedValue === 'true')
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const mediaQuery = window.matchMedia('(max-width: 768px)')
|
|
100
|
+
const syncViewport = () => setIsMobile(mediaQuery.matches)
|
|
101
|
+
|
|
102
|
+
syncViewport()
|
|
103
|
+
mediaQuery.addEventListener('change', syncViewport)
|
|
104
|
+
|
|
105
|
+
return () => mediaQuery.removeEventListener('change', syncViewport)
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
document.documentElement.style.setProperty('--nav-width', isMini ? '88px' : '280px')
|
|
110
|
+
window.localStorage.setItem('minimal-admin-nav-mini', String(isMini))
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
document.documentElement.style.removeProperty('--nav-width')
|
|
114
|
+
}
|
|
115
|
+
}, [isMini])
|
|
116
|
+
|
|
117
|
+
const userLabel = user?.name?.trim() || user?.email?.split('@')[0] || 'Quản trị viên'
|
|
118
|
+
const initials = useMemo(() => getInitials(user?.name, user?.email), [user?.email, user?.name])
|
|
119
|
+
|
|
120
|
+
const closeMobileNav = () => {
|
|
121
|
+
if (window.matchMedia('(max-width: 768px)').matches) setNavOpen(false)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const classes = [
|
|
125
|
+
'minimal-nav',
|
|
126
|
+
navOpen && 'minimal-nav--open',
|
|
127
|
+
isMini && 'minimal-nav--mini',
|
|
128
|
+
shouldAnimate && 'minimal-nav--animate',
|
|
129
|
+
hydrated && 'minimal-nav--hydrated',
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(' ')
|
|
133
|
+
|
|
134
|
+
const isActive = (href: string) =>
|
|
135
|
+
pathname === href || (href !== adminRoute && pathname.startsWith(`${href}/`))
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<aside
|
|
139
|
+
aria-label="Điều hướng quản trị"
|
|
140
|
+
className={classes}
|
|
141
|
+
inert={(isMobile && !navOpen) || undefined}
|
|
142
|
+
>
|
|
143
|
+
<div className="minimal-nav__scroll" ref={navRef}>
|
|
144
|
+
<div className="minimal-nav__brand-row">
|
|
145
|
+
<Link
|
|
146
|
+
aria-label={`Về dashboard ${brand.siteName}`}
|
|
147
|
+
className="minimal-nav__brand"
|
|
148
|
+
href={adminRoute}
|
|
149
|
+
onClick={closeMobileNav}
|
|
150
|
+
prefetch={false}
|
|
151
|
+
>
|
|
152
|
+
<span className="minimal-nav__brand-full">
|
|
153
|
+
<AdminLogo />
|
|
154
|
+
</span>
|
|
155
|
+
<span className="minimal-nav__brand-icon">
|
|
156
|
+
<AdminIcon />
|
|
157
|
+
</span>
|
|
158
|
+
</Link>
|
|
159
|
+
<button
|
|
160
|
+
aria-label="Đóng menu"
|
|
161
|
+
className="minimal-nav__mobile-close"
|
|
162
|
+
onClick={() => setNavOpen(false)}
|
|
163
|
+
type="button"
|
|
164
|
+
>
|
|
165
|
+
<X aria-hidden="true" />
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<Link
|
|
170
|
+
className="minimal-nav__workspace"
|
|
171
|
+
href={adminRoute}
|
|
172
|
+
onClick={closeMobileNav}
|
|
173
|
+
prefetch={false}
|
|
174
|
+
title={brand.siteName}
|
|
175
|
+
>
|
|
176
|
+
<span className="minimal-nav__workspace-icon">
|
|
177
|
+
<GalleryVerticalEnd aria-hidden="true" />
|
|
178
|
+
</span>
|
|
179
|
+
<span className="minimal-nav__workspace-copy">
|
|
180
|
+
<small>Không gian làm việc</small>
|
|
181
|
+
<strong>{brand.siteName}</strong>
|
|
182
|
+
</span>
|
|
183
|
+
<Menu aria-hidden="true" className="minimal-nav__workspace-menu" />
|
|
184
|
+
</Link>
|
|
185
|
+
|
|
186
|
+
<nav className="minimal-nav__menu">
|
|
187
|
+
<div className="minimal-nav__group">
|
|
188
|
+
<span className="minimal-nav__group-label">Tổng quan</span>
|
|
189
|
+
<Link
|
|
190
|
+
aria-current={pathname === adminRoute ? 'page' : undefined}
|
|
191
|
+
className={`minimal-nav__item ${pathname === adminRoute ? 'minimal-nav__item--active' : ''}`}
|
|
192
|
+
href={adminRoute}
|
|
193
|
+
onClick={closeMobileNav}
|
|
194
|
+
prefetch={false}
|
|
195
|
+
title="Dashboard"
|
|
196
|
+
>
|
|
197
|
+
<LayoutDashboard aria-hidden="true" />
|
|
198
|
+
<span>Tổng quan</span>
|
|
199
|
+
</Link>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
{groups.map((group) => (
|
|
203
|
+
<div className="minimal-nav__group" key={group.label}>
|
|
204
|
+
<span className="minimal-nav__group-label">{translateGroupLabel(group.label)}</span>
|
|
205
|
+
{group.items.map((item) => {
|
|
206
|
+
const href = formatAdminURL({
|
|
207
|
+
adminRoute,
|
|
208
|
+
path: `/${item.type}/${item.slug}`,
|
|
209
|
+
})
|
|
210
|
+
const Icon = iconBySlug[item.slug] || FileText
|
|
211
|
+
const active = isActive(href)
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<Link
|
|
215
|
+
aria-current={active ? 'page' : undefined}
|
|
216
|
+
className={`minimal-nav__item ${active ? 'minimal-nav__item--active' : ''}`}
|
|
217
|
+
href={href}
|
|
218
|
+
key={`${item.type}-${item.slug}`}
|
|
219
|
+
onClick={closeMobileNav}
|
|
220
|
+
prefetch={false}
|
|
221
|
+
title={item.label}
|
|
222
|
+
>
|
|
223
|
+
<Icon aria-hidden="true" />
|
|
224
|
+
<span>{item.label}</span>
|
|
225
|
+
</Link>
|
|
226
|
+
)
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
))}
|
|
230
|
+
|
|
231
|
+
{folders && folders.browseByFolder && (
|
|
232
|
+
<div className="minimal-nav__group">
|
|
233
|
+
<span className="minimal-nav__group-label">Thư viện</span>
|
|
234
|
+
<Link
|
|
235
|
+
aria-current={
|
|
236
|
+
isActive(formatAdminURL({ adminRoute, path: adminRoutes.browseByFolder }))
|
|
237
|
+
? 'page'
|
|
238
|
+
: undefined
|
|
239
|
+
}
|
|
240
|
+
className={`minimal-nav__item ${
|
|
241
|
+
isActive(formatAdminURL({ adminRoute, path: adminRoutes.browseByFolder }))
|
|
242
|
+
? 'minimal-nav__item--active'
|
|
243
|
+
: ''
|
|
244
|
+
}`}
|
|
245
|
+
href={formatAdminURL({ adminRoute, path: adminRoutes.browseByFolder })}
|
|
246
|
+
onClick={closeMobileNav}
|
|
247
|
+
prefetch={false}
|
|
248
|
+
title="Duyệt theo thư mục"
|
|
249
|
+
>
|
|
250
|
+
<Library aria-hidden="true" />
|
|
251
|
+
<span>Duyệt theo thư mục</span>
|
|
252
|
+
</Link>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</nav>
|
|
256
|
+
|
|
257
|
+
<div className="minimal-nav__footer">
|
|
258
|
+
<Link
|
|
259
|
+
className="minimal-nav__account"
|
|
260
|
+
href={formatAdminURL({ adminRoute, path: adminRoutes.account })}
|
|
261
|
+
onClick={closeMobileNav}
|
|
262
|
+
prefetch={false}
|
|
263
|
+
title={userLabel}
|
|
264
|
+
>
|
|
265
|
+
<span className="minimal-nav__avatar">{initials}</span>
|
|
266
|
+
<span className="minimal-nav__account-copy">
|
|
267
|
+
<strong>{userLabel}</strong>
|
|
268
|
+
<small>{user?.email || 'Tài khoản quản trị'}</small>
|
|
269
|
+
</span>
|
|
270
|
+
</Link>
|
|
271
|
+
<div className="minimal-nav__logout">
|
|
272
|
+
<AdminLogoutButton />
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<button
|
|
278
|
+
aria-label={isMini ? 'Mở rộng menu' : 'Thu gọn menu'}
|
|
279
|
+
className="minimal-nav__collapse"
|
|
280
|
+
onClick={() => setIsMini((current) => !current)}
|
|
281
|
+
title={isMini ? 'Mở rộng menu' : 'Thu gọn menu'}
|
|
282
|
+
type="button"
|
|
283
|
+
>
|
|
284
|
+
{isMini ? <ChevronRight aria-hidden="true" /> : <ChevronLeft aria-hidden="true" />}
|
|
285
|
+
</button>
|
|
286
|
+
</aside>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '@payloadcms/ui'
|
|
4
|
+
import { Moon, Sun } from 'lucide-react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
const AdminThemeToggle: React.FC = () => {
|
|
8
|
+
const { setTheme, theme } = useTheme()
|
|
9
|
+
const isDark = theme === 'dark'
|
|
10
|
+
const label = isDark ? 'Chuyển sang giao diện sáng' : 'Chuyển sang giao diện tối'
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<button
|
|
14
|
+
aria-checked={isDark}
|
|
15
|
+
aria-label={label}
|
|
16
|
+
className="admin-theme-toggle"
|
|
17
|
+
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
|
18
|
+
role="switch"
|
|
19
|
+
title={label}
|
|
20
|
+
type="button"
|
|
21
|
+
>
|
|
22
|
+
<span aria-hidden="true" className="admin-theme-toggle__track">
|
|
23
|
+
<span className="admin-theme-toggle__thumb">
|
|
24
|
+
{isDark ? <Moon strokeWidth={2.2} /> : <Sun strokeWidth={2.2} />}
|
|
25
|
+
</span>
|
|
26
|
+
</span>
|
|
27
|
+
</button>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default AdminThemeToggle
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ArrowUpRight } from 'lucide-react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
const AdminViewSite: React.FC = () => (
|
|
5
|
+
<a className="admin-view-site" data-slot="button" href="/" rel="noreferrer" target="_blank">
|
|
6
|
+
Xem website
|
|
7
|
+
<ArrowUpRight aria-hidden="true" />
|
|
8
|
+
</a>
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export default AdminViewSite
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { AdminBrandAsset } from './Brand'
|
|
6
|
+
|
|
7
|
+
export type AdminBrandSettings = {
|
|
8
|
+
logo: AdminBrandAsset
|
|
9
|
+
siteName: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type SettingsResponse = {
|
|
13
|
+
logo?:
|
|
14
|
+
| {
|
|
15
|
+
alt?: null | string
|
|
16
|
+
height?: null | number
|
|
17
|
+
updatedAt?: null | string
|
|
18
|
+
url?: null | string
|
|
19
|
+
width?: null | number
|
|
20
|
+
}
|
|
21
|
+
| number
|
|
22
|
+
| null
|
|
23
|
+
siteName?: null | string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let defaultBrand: AdminBrandSettings = {
|
|
27
|
+
logo: { alt: 'Wikex', height: 64, url: '/favicon.svg', width: 64 },
|
|
28
|
+
siteName: 'Wikex',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const configureAdminBrand = (brand: AdminBrandSettings) => {
|
|
32
|
+
defaultBrand = brand
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalizeBrand = (settings?: SettingsResponse | null): AdminBrandSettings => {
|
|
36
|
+
const logo = settings?.logo
|
|
37
|
+
const resolvedLogo =
|
|
38
|
+
logo && typeof logo !== 'number' && logo.url
|
|
39
|
+
? {
|
|
40
|
+
alt: logo.alt || settings?.siteName || defaultBrand.siteName,
|
|
41
|
+
height: logo.height || 64,
|
|
42
|
+
url: logo.updatedAt ? `${logo.url}?${encodeURIComponent(logo.updatedAt)}` : logo.url,
|
|
43
|
+
width: logo.width || 240,
|
|
44
|
+
}
|
|
45
|
+
: defaultBrand.logo
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
logo: resolvedLogo,
|
|
49
|
+
siteName: settings?.siteName?.trim() || defaultBrand.siteName,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const useAdminBrand = (): AdminBrandSettings => {
|
|
54
|
+
const [brand, setBrand] = useState<AdminBrandSettings>(() => normalizeBrand())
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const controller = new AbortController()
|
|
58
|
+
|
|
59
|
+
const loadBrand = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch('/api/globals/settings?depth=1', {
|
|
62
|
+
cache: 'no-store',
|
|
63
|
+
signal: controller.signal,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (!response.ok) return
|
|
67
|
+
setBrand(normalizeBrand((await response.json()) as SettingsResponse))
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) {
|
|
70
|
+
console.error('Unable to load admin branding', error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void loadBrand()
|
|
76
|
+
|
|
77
|
+
return () => controller.abort()
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
return brand
|
|
81
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Config, Field, Plugin } from 'payload'
|
|
2
|
+
|
|
3
|
+
export const IMAGE_UPLOAD_GUIDANCE =
|
|
4
|
+
'Yêu cầu để ảnh hiển thị rõ: hero/banner nên từ 1920 × 1080 px; ảnh nội dung nên rộng tối thiểu 1200 px. Ưu tiên WebP hoặc JPEG, dung lượng dưới 500 KB.'
|
|
5
|
+
|
|
6
|
+
const labelTranslations: Record<string, string> = {
|
|
7
|
+
'Archive Block': 'Khối lưu trữ',
|
|
8
|
+
Content: 'Nội dung',
|
|
9
|
+
Description: 'Mô tả',
|
|
10
|
+
Hero: 'Mở đầu',
|
|
11
|
+
'Hero Image': 'Ảnh đại diện',
|
|
12
|
+
Image: 'Hình ảnh',
|
|
13
|
+
Layout: 'Bố cục',
|
|
14
|
+
Link: 'Liên kết',
|
|
15
|
+
Media: 'Hình ảnh',
|
|
16
|
+
Meta: 'Thông tin bổ sung',
|
|
17
|
+
Title: 'Tiêu đề',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fieldNameLabels: Record<string, string> = {
|
|
21
|
+
alt: 'Văn bản thay thế',
|
|
22
|
+
authors: 'Tác giả',
|
|
23
|
+
caption: 'Chú thích',
|
|
24
|
+
categories: 'Chuyên mục',
|
|
25
|
+
content: 'Nội dung',
|
|
26
|
+
coverImage: 'Ảnh bìa',
|
|
27
|
+
defaultOgImage: 'Ảnh chia sẻ mặc định',
|
|
28
|
+
description: 'Mô tả',
|
|
29
|
+
excerpt: 'Tóm tắt',
|
|
30
|
+
featuredImage: 'Ảnh nổi bật',
|
|
31
|
+
heroImage: 'Ảnh đại diện',
|
|
32
|
+
image: 'Hình ảnh',
|
|
33
|
+
logo: 'Logo',
|
|
34
|
+
media: 'Hình ảnh',
|
|
35
|
+
appearance: 'Kiểu hiển thị',
|
|
36
|
+
links: 'Liên kết',
|
|
37
|
+
newTab: 'Mở trong tab mới',
|
|
38
|
+
publishedAt: 'Ngày xuất bản',
|
|
39
|
+
relatedPosts: 'Bài viết liên quan',
|
|
40
|
+
slug: 'Đường dẫn',
|
|
41
|
+
summary: 'Mô tả ngắn',
|
|
42
|
+
title: 'Tiêu đề',
|
|
43
|
+
type: 'Loại',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entityLabels: Record<string, { plural: string; singular: string }> = {
|
|
47
|
+
'form-submissions': { plural: 'Dữ liệu biểu mẫu', singular: 'Dữ liệu biểu mẫu' },
|
|
48
|
+
forms: { plural: 'Biểu mẫu', singular: 'Biểu mẫu' },
|
|
49
|
+
redirects: { plural: 'Chuyển hướng', singular: 'Chuyển hướng' },
|
|
50
|
+
search: { plural: 'Kết quả tìm kiếm', singular: 'Kết quả tìm kiếm' },
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const translateLabel = <T>(label: T): T =>
|
|
54
|
+
(typeof label === 'string' ? labelTranslations[label] || label : label) as T
|
|
55
|
+
|
|
56
|
+
const enhanceFields = (fields: Field[]) => {
|
|
57
|
+
for (const field of fields) {
|
|
58
|
+
if ('name' in field) {
|
|
59
|
+
field.label =
|
|
60
|
+
field.label === undefined ? fieldNameLabels[field.name] : translateLabel(field.label)
|
|
61
|
+
} else if ('label' in field) {
|
|
62
|
+
field.label = translateLabel(field.label)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (field.type === 'upload' && field.relationTo === 'media') {
|
|
66
|
+
const currentDescription = field.admin?.description
|
|
67
|
+
field.admin = {
|
|
68
|
+
...field.admin,
|
|
69
|
+
description:
|
|
70
|
+
typeof currentDescription === 'string' &&
|
|
71
|
+
!currentDescription.includes(IMAGE_UPLOAD_GUIDANCE)
|
|
72
|
+
? `${currentDescription} ${IMAGE_UPLOAD_GUIDANCE}`
|
|
73
|
+
: IMAGE_UPLOAD_GUIDANCE,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (field.type === 'tabs') {
|
|
78
|
+
for (const tab of field.tabs) {
|
|
79
|
+
tab.label = translateLabel(tab.label)
|
|
80
|
+
enhanceFields(tab.fields)
|
|
81
|
+
}
|
|
82
|
+
} else if ('fields' in field && Array.isArray(field.fields)) {
|
|
83
|
+
enhanceFields(field.fields)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (field.type === 'blocks') {
|
|
87
|
+
for (const block of field.blocks ?? []) {
|
|
88
|
+
if (block.labels) {
|
|
89
|
+
block.labels = {
|
|
90
|
+
plural: translateLabel(block.labels.plural),
|
|
91
|
+
singular: translateLabel(block.labels.singular),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
enhanceFields(block.fields)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const adminExperiencePlugin: Plugin = (config: Config): Config => {
|
|
101
|
+
for (const collection of config.collections ?? []) {
|
|
102
|
+
const translatedLabels = entityLabels[collection.slug]
|
|
103
|
+
if (translatedLabels) collection.labels = translatedLabels
|
|
104
|
+
enhanceFields(collection.fields)
|
|
105
|
+
|
|
106
|
+
if (collection.slug === 'media') {
|
|
107
|
+
collection.admin = {
|
|
108
|
+
...collection.admin,
|
|
109
|
+
description: IMAGE_UPLOAD_GUIDANCE,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const global of config.globals ?? []) enhanceFields(global.fields)
|
|
115
|
+
|
|
116
|
+
return config
|
|
117
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { adminExperiencePlugin, IMAGE_UPLOAD_GUIDANCE } from './adminExperience'
|
|
2
|
+
export { wikexAdminPlugin, type WikexAdminPluginOptions } from './plugin'
|
|
3
|
+
export {
|
|
4
|
+
createVisualEditorRouteResolver,
|
|
5
|
+
defineWikexProject,
|
|
6
|
+
type VisualEditorRouteRegistry,
|
|
7
|
+
type WikexProjectDefinition,
|
|
8
|
+
} from './project'
|
|
9
|
+
export { RichTextTypographyFeature } from './rich-text'
|
|
10
|
+
export { TextStyleRuntime } from './TextStyleRuntime'
|
|
11
|
+
export * from './textStyles'
|