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 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 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 { const records = await listResource('sitemap-menus') return records.map(normalizeSitemapMenu).filter(menu => menu.id && menu.name) } export async function createSitemapMenu(payload: Pick): Promise { const created = await apiRequest('/api/sitemap-menus/', { method: 'POST', body: JSON.stringify(payload), }) return normalizeSitemapMenu(created) } export async function updateSitemapMenu(id: string, payload: Pick): Promise { const updated = await apiRequest(`/api/sitemap-menus/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify(payload), }) return normalizeSitemapMenu(updated) } export async function removeSitemapMenu(id: string): Promise { await apiRequest(`/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 { const [pagesRaw, postsRaw, coursesRaw] = await Promise.all([ listResource('pages'), apiRequest('/api/blog/posts'), listResource('live-courses'), ]) const pages = pagesRaw .map(raw => { const source = (raw ?? {}) as Record 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 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 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('/')) }