223 lines
6.8 KiB
TypeScript
223 lines
6.8 KiB
TypeScript
import { apiRequest, listResource } from './api/client'
|
|
|
|
export type SitemapTargetType = 'page' | 'post' | 'course'
|
|
|
|
export type SitemapMenuItem = {
|
|
id: string
|
|
label: string
|
|
targetType: SitemapTargetType
|
|
targetId: string
|
|
to: string
|
|
children: SitemapMenuItem[]
|
|
}
|
|
|
|
export type SitemapMenu = {
|
|
id: string
|
|
name: string
|
|
description?: string
|
|
items: SitemapMenuItem[]
|
|
createdAt?: number
|
|
updatedAt?: number
|
|
}
|
|
|
|
export type SitemapSourceOption = {
|
|
id: string
|
|
label: string
|
|
slug?: string
|
|
}
|
|
|
|
export type SitemapSources = {
|
|
pages: SitemapSourceOption[]
|
|
posts: SitemapSourceOption[]
|
|
courses: SitemapSourceOption[]
|
|
}
|
|
|
|
function asString(value: unknown): string {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|
|
|
|
function asNumber(value: unknown): number | undefined {
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
return undefined
|
|
}
|
|
|
|
function normalizeItem(raw: unknown): SitemapMenuItem {
|
|
const source = (raw ?? {}) as Record<string, unknown>
|
|
const children = Array.isArray(source.children) ? source.children.map(normalizeItem) : []
|
|
|
|
const targetTypeRaw = asString(source.targetType)
|
|
const targetType: SitemapTargetType = targetTypeRaw === 'post' || targetTypeRaw === 'course' ? targetTypeRaw : 'page'
|
|
|
|
return {
|
|
id: asString(source.id),
|
|
label: asString(source.label),
|
|
targetType,
|
|
targetId: asString(source.targetId),
|
|
to: asString(source.to),
|
|
children,
|
|
}
|
|
}
|
|
|
|
export function normalizeSitemapMenu(raw: unknown): SitemapMenu {
|
|
const source = (raw ?? {}) as Record<string, unknown>
|
|
const items = Array.isArray(source.items) ? source.items.map(normalizeItem) : []
|
|
|
|
return {
|
|
id: asString(source.id),
|
|
name: asString(source.name),
|
|
description: asString(source.description) || undefined,
|
|
items,
|
|
createdAt: asNumber(source.createdAt),
|
|
updatedAt: asNumber(source.updatedAt),
|
|
}
|
|
}
|
|
|
|
export async function fetchSitemapMenus(): Promise<SitemapMenu[]> {
|
|
const records = await listResource<unknown>('sitemap-menus')
|
|
return records.map(normalizeSitemapMenu).filter(menu => menu.id && menu.name)
|
|
}
|
|
|
|
export async function createSitemapMenu(payload: Pick<SitemapMenu, 'name' | 'description' | 'items'>): Promise<SitemapMenu> {
|
|
const created = await apiRequest<unknown>('/api/sitemap-menus/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
return normalizeSitemapMenu(created)
|
|
}
|
|
|
|
export async function updateSitemapMenu(id: string, payload: Pick<SitemapMenu, 'name' | 'description' | 'items'>): Promise<SitemapMenu> {
|
|
const updated = await apiRequest<unknown>(`/api/sitemap-menus/${encodeURIComponent(id)}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
})
|
|
return normalizeSitemapMenu(updated)
|
|
}
|
|
|
|
export async function removeSitemapMenu(id: string): Promise<void> {
|
|
await apiRequest<void>(`/api/sitemap-menus/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
})
|
|
}
|
|
|
|
export function buildPathForSource(targetType: SitemapTargetType, option: SitemapSourceOption): string {
|
|
if (targetType === 'page') {
|
|
return '/admin/builder'
|
|
}
|
|
if (targetType === 'post') {
|
|
return option.slug ? `/blog/${option.slug}` : '/blog'
|
|
}
|
|
return option.slug ? `/live-courses/${option.slug}` : '/live-courses'
|
|
}
|
|
|
|
export async function fetchSitemapSources(): Promise<SitemapSources> {
|
|
const [pagesRaw, postsRaw, coursesRaw] = await Promise.all([
|
|
listResource<unknown>('pages'),
|
|
apiRequest<unknown[]>('/api/blog/posts'),
|
|
listResource<unknown>('live-courses'),
|
|
])
|
|
|
|
const pages = pagesRaw
|
|
.map(raw => {
|
|
const source = (raw ?? {}) as Record<string, unknown>
|
|
return {
|
|
id: asString(source.id),
|
|
label: asString(source.name) || asString(source.slug) || asString(source.id),
|
|
slug: asString(source.slug) || undefined,
|
|
}
|
|
})
|
|
.filter(item => item.id && item.label)
|
|
|
|
const posts = postsRaw
|
|
.map(raw => {
|
|
const source = (raw ?? {}) as Record<string, unknown>
|
|
return {
|
|
id: asString(source.id),
|
|
label: asString(source.title) || asString(source.slug) || asString(source.id),
|
|
slug: asString(source.slug) || undefined,
|
|
}
|
|
})
|
|
.filter(item => item.id && item.label)
|
|
|
|
const courses = coursesRaw
|
|
.map(raw => {
|
|
const source = (raw ?? {}) as Record<string, unknown>
|
|
return {
|
|
id: asString(source.id),
|
|
label: asString(source.title) || asString(source.slug) || asString(source.id),
|
|
slug: asString(source.slug) || undefined,
|
|
}
|
|
})
|
|
.filter(item => item.id && item.label)
|
|
|
|
return { pages, posts, courses }
|
|
}
|
|
|
|
export type MenuParentOption = {
|
|
id: string
|
|
label: string
|
|
}
|
|
|
|
export function flattenParentOptions(items: SitemapMenuItem[], depth = 0): MenuParentOption[] {
|
|
const options: MenuParentOption[] = []
|
|
for (const item of items) {
|
|
options.push({ id: item.id, label: `${' '.repeat(depth * 2)}${item.label}`.trimStart() })
|
|
options.push(...flattenParentOptions(item.children, depth + 1))
|
|
}
|
|
return options
|
|
}
|
|
|
|
export function addMenuItem(items: SitemapMenuItem[], parentId: string | null, newItem: SitemapMenuItem): SitemapMenuItem[] {
|
|
if (!parentId) {
|
|
return [...items, newItem]
|
|
}
|
|
|
|
return items.map(item => {
|
|
if (item.id === parentId) {
|
|
return { ...item, children: [...item.children, newItem] }
|
|
}
|
|
return { ...item, children: addMenuItem(item.children, parentId, newItem) }
|
|
})
|
|
}
|
|
|
|
export function removeMenuItem(items: SitemapMenuItem[], itemId: string): SitemapMenuItem[] {
|
|
return items
|
|
.filter(item => item.id !== itemId)
|
|
.map(item => ({ ...item, children: removeMenuItem(item.children, itemId) }))
|
|
}
|
|
|
|
function moveWithinSiblings(items: SitemapMenuItem[], itemId: string, direction: 'up' | 'down'): SitemapMenuItem[] {
|
|
const index = items.findIndex(item => item.id === itemId)
|
|
if (index === -1) {
|
|
return items.map(item => ({ ...item, children: moveWithinSiblings(item.children, itemId, direction) }))
|
|
}
|
|
|
|
const target = direction === 'up' ? index - 1 : index + 1
|
|
if (target < 0 || target >= items.length) {
|
|
return items
|
|
}
|
|
|
|
const next = [...items]
|
|
const [entry] = next.splice(index, 1)
|
|
next.splice(target, 0, entry)
|
|
return next
|
|
}
|
|
|
|
export function moveMenuItem(items: SitemapMenuItem[], itemId: string, direction: 'up' | 'down'): SitemapMenuItem[] {
|
|
return moveWithinSiblings(items, itemId, direction)
|
|
}
|
|
|
|
export function updateMenuItemLabel(items: SitemapMenuItem[], itemId: string, label: string): SitemapMenuItem[] {
|
|
return items.map(item => {
|
|
if (item.id === itemId) {
|
|
return { ...item, label }
|
|
}
|
|
return { ...item, children: updateMenuItemLabel(item.children, itemId, label) }
|
|
})
|
|
}
|
|
|
|
export function toHomeNavItems(items: SitemapMenuItem[]): Array<{ id: string; label: string; to: string }> {
|
|
return items
|
|
.map(item => ({ id: item.id, label: item.label, to: item.to }))
|
|
.filter(item => item.id && item.label && item.to.startsWith('/'))
|
|
}
|