192 lines
7.4 KiB
TypeScript
Vendored
192 lines
7.4 KiB
TypeScript
Vendored
import { useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { BookOpen, ChevronRight, ChevronDown, Plus, Settings, Menu } from 'lucide-react'
|
|
import PageHeader from '@/components/common/PageHeader'
|
|
import SearchInput from '@/components/common/SearchInput'
|
|
import LoadingSpinner from '@/components/common/LoadingSpinner'
|
|
import { usePermission } from '@/hooks/usePermission'
|
|
import { getCategories, getDocs } from '@/api/doc'
|
|
import { formatDate } from '@/lib/utils'
|
|
import { cn } from '@/lib/utils'
|
|
import DocCategoryManager from './DocCategoryManager'
|
|
|
|
export default function DocCenter() {
|
|
const { t } = useTranslation()
|
|
const navigate = useNavigate()
|
|
const { isAdmin } = usePermission()
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
|
const [search, setSearch] = useState('')
|
|
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set())
|
|
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
const [categoryManagerOpen, setCategoryManagerOpen] = useState(false)
|
|
|
|
const { data: categories = [], isLoading: catLoading } = useQuery({
|
|
queryKey: ['doc-categories'],
|
|
queryFn: getCategories,
|
|
})
|
|
|
|
const { data: docsData, isLoading: docsLoading } = useQuery({
|
|
queryKey: ['docs', selectedCategory, search],
|
|
queryFn: () => getDocs({ category: selectedCategory || undefined, search: search || undefined }),
|
|
})
|
|
|
|
const toggleExpand = (name: string) => {
|
|
setExpandedCats((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(name)) next.delete(name)
|
|
else next.add(name)
|
|
return next
|
|
})
|
|
}
|
|
|
|
const docs = docsData?.data ?? []
|
|
|
|
const topCategories = categories.filter((c) => !c.parent_id)
|
|
const getChildren = (parentId: number) => categories.filter((c) => c.parent_id === parentId)
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<PageHeader
|
|
title={t('doc.title')}
|
|
description={t('doc.description')}
|
|
actions={
|
|
isAdmin ? (
|
|
<div className="flex gap-2">
|
|
<button className="btn btn-sm btn-primary" onClick={() => navigate('/docs/new/edit')}>
|
|
<Plus size={16} /> {t('doc.createDoc')}
|
|
</button>
|
|
<button className="btn btn-sm btn-ghost" onClick={() => setCategoryManagerOpen(true)}>
|
|
<Settings size={16} /> {t('doc.manageCategories')}
|
|
</button>
|
|
</div>
|
|
) : undefined
|
|
}
|
|
/>
|
|
|
|
<div className="flex flex-1 gap-4 min-h-0">
|
|
{/* Mobile drawer toggle */}
|
|
<button
|
|
className="btn btn-sm btn-ghost lg:hidden fixed top-20 left-4 z-40"
|
|
onClick={() => setDrawerOpen(!drawerOpen)}
|
|
>
|
|
<Menu size={18} />
|
|
</button>
|
|
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={cn(
|
|
'w-64 shrink-0 bg-base-200 rounded-box p-4 overflow-y-auto',
|
|
'fixed inset-y-0 left-0 z-30 transition-transform lg:relative lg:translate-x-0',
|
|
drawerOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
|
)}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className="font-semibold text-sm">{t('doc.categories')}</h3>
|
|
<button className="btn btn-xs btn-ghost lg:hidden" onClick={() => setDrawerOpen(false)}>
|
|
{t('common.close')}
|
|
</button>
|
|
</div>
|
|
|
|
{catLoading ? (
|
|
<LoadingSpinner size="sm" />
|
|
) : (
|
|
<ul className="menu menu-sm gap-0.5">
|
|
<li>
|
|
<button
|
|
className={cn(!selectedCategory && 'active')}
|
|
onClick={() => { setSelectedCategory(null); setDrawerOpen(false) }}
|
|
>
|
|
<BookOpen size={14} /> {t('doc.allDocs')}
|
|
</button>
|
|
</li>
|
|
{topCategories.map((cat) => {
|
|
const children = getChildren(cat.id)
|
|
const expanded = expandedCats.has(cat.name)
|
|
return (
|
|
<li key={cat.id}>
|
|
<button
|
|
className={cn(selectedCategory === cat.name && 'active')}
|
|
onClick={() => { setSelectedCategory(cat.name); setDrawerOpen(false) }}
|
|
>
|
|
{children.length > 0 ? (
|
|
<span className="cursor-pointer" onClick={(e) => { e.stopPropagation(); toggleExpand(cat.name) }}>
|
|
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
</span>
|
|
) : (
|
|
<BookOpen size={14} />
|
|
)}
|
|
{cat.label}
|
|
</button>
|
|
{expanded && children.length > 0 && (
|
|
<ul>
|
|
{children.map((child) => (
|
|
<li key={child.id}>
|
|
<button
|
|
className={cn(selectedCategory === child.name && 'active')}
|
|
onClick={() => { setSelectedCategory(child.name); setDrawerOpen(false) }}
|
|
>
|
|
<BookOpen size={12} /> {child.label}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
)}
|
|
</aside>
|
|
|
|
{/* Overlay for mobile drawer */}
|
|
{drawerOpen && (
|
|
<div className="fixed inset-0 bg-black/30 z-20 lg:hidden" onClick={() => setDrawerOpen(false)} />
|
|
)}
|
|
|
|
{/* Content */}
|
|
<main className="flex-1 min-w-0">
|
|
<div className="mb-4">
|
|
<SearchInput value={search} onChange={setSearch} placeholder={t('doc.searchPlaceholder')} />
|
|
</div>
|
|
|
|
{docsLoading ? (
|
|
<LoadingSpinner text={t('common.loading')} />
|
|
) : docs.length === 0 ? (
|
|
<div className="text-center py-16 text-base-content/50">
|
|
<BookOpen size={48} className="mx-auto mb-4 opacity-30" />
|
|
<p>{t('common.noData')}</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-3">
|
|
{docs.map((doc) => (
|
|
<div
|
|
key={doc.id}
|
|
className="card card-compact bg-base-200 hover:bg-base-300 cursor-pointer transition-colors"
|
|
onClick={() => navigate(`/docs/${doc.slug}`)}
|
|
>
|
|
<div className="card-body">
|
|
<h3 className="card-title text-base">{doc.title}</h3>
|
|
{doc.excerpt && (
|
|
<p className="text-sm text-base-content/60 line-clamp-2">{doc.excerpt}</p>
|
|
)}
|
|
<div className="flex items-center gap-3 text-xs text-base-content/50 mt-1">
|
|
{doc.category && (
|
|
<span className="badge badge-sm badge-ghost">{doc.category}</span>
|
|
)}
|
|
<span>{formatDate(doc.updated_at)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
|
|
<DocCategoryManager open={categoryManagerOpen} onClose={() => setCategoryManagerOpen(false)} />
|
|
</div>
|
|
)
|
|
}
|