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('/'))
}