213 lines
7.1 KiB
JavaScript
213 lines
7.1 KiB
JavaScript
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
import { Container } from 'react-bootstrap'
|
|
import { Link } from 'react-router-dom'
|
|
import { fetchUserSetting, listAllForums, saveUserSetting } from '../api/client'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useAuth } from '../context/AuthContext'
|
|
|
|
export default function BoardIndex() {
|
|
const [forums, setForums] = useState([])
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(true)
|
|
const [collapsed, setCollapsed] = useState({})
|
|
const { t } = useTranslation()
|
|
const { token } = useAuth()
|
|
const collapsedKey = 'board_index.collapsed_categories'
|
|
const storageKey = `speedbb_user_setting_${collapsedKey}`
|
|
const saveTimer = useRef(null)
|
|
|
|
useEffect(() => {
|
|
listAllForums()
|
|
.then(setForums)
|
|
.catch((err) => setError(err.message))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!token) return
|
|
let active = true
|
|
|
|
const cached = localStorage.getItem(storageKey)
|
|
if (cached) {
|
|
try {
|
|
const parsed = JSON.parse(cached)
|
|
if (Array.isArray(parsed)) {
|
|
const next = {}
|
|
parsed.forEach((id) => {
|
|
next[String(id)] = true
|
|
})
|
|
setCollapsed(next)
|
|
}
|
|
} catch {
|
|
localStorage.removeItem(storageKey)
|
|
}
|
|
}
|
|
|
|
fetchUserSetting(collapsedKey)
|
|
.then((setting) => {
|
|
if (!active) return
|
|
const next = {}
|
|
if (Array.isArray(setting?.value)) {
|
|
setting.value.forEach((id) => {
|
|
next[String(id)] = true
|
|
})
|
|
}
|
|
setCollapsed(next)
|
|
localStorage.setItem(storageKey, JSON.stringify(setting?.value || []))
|
|
})
|
|
.catch(() => {})
|
|
|
|
return () => {
|
|
active = false
|
|
}
|
|
}, [token])
|
|
|
|
const getParentId = (forum) => {
|
|
if (!forum.parent) return null
|
|
if (typeof forum.parent === 'string') {
|
|
return forum.parent.split('/').pop()
|
|
}
|
|
return forum.parent.id ?? null
|
|
}
|
|
|
|
const forumTree = useMemo(() => {
|
|
const map = new Map()
|
|
const roots = []
|
|
|
|
forums.forEach((forum) => {
|
|
map.set(String(forum.id), { ...forum, children: [] })
|
|
})
|
|
|
|
forums.forEach((forum) => {
|
|
const parentId = getParentId(forum)
|
|
const node = map.get(String(forum.id))
|
|
if (parentId && map.has(String(parentId))) {
|
|
map.get(String(parentId)).children.push(node)
|
|
} else {
|
|
roots.push(node)
|
|
}
|
|
})
|
|
|
|
const sortNodes = (nodes) => {
|
|
nodes.sort((a, b) => {
|
|
if (a.position !== b.position) return a.position - b.position
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
nodes.forEach((node) => sortNodes(node.children))
|
|
}
|
|
|
|
sortNodes(roots)
|
|
|
|
return roots
|
|
}, [forums])
|
|
|
|
const renderRows = (nodes) =>
|
|
nodes.map((node) => (
|
|
<div className="bb-board-row" key={node.id}>
|
|
<div className="bb-board-cell bb-board-cell--title">
|
|
<div className="bb-board-title">
|
|
<span className="bb-board-icon" aria-hidden="true">
|
|
<i className={`bi ${node.type === 'category' ? 'bi-folder2' : 'bi-chat-left-text'}`} />
|
|
</span>
|
|
<div>
|
|
<Link to={`/forum/${node.id}`} className="bb-board-link">
|
|
{node.name}
|
|
</Link>
|
|
<div className="bb-board-desc">{node.description || t('forum.no_description')}</div>
|
|
{node.children?.length > 0 && (
|
|
<div className="bb-board-subforums">
|
|
{t('forum.children')}:{' '}
|
|
{node.children.map((child, index) => (
|
|
<span key={child.id}>
|
|
<Link to={`/forum/${child.id}`} className="bb-board-subforum-link">
|
|
{child.name}
|
|
</Link>
|
|
{index < node.children.length - 1 ? ', ' : ''}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bb-board-cell bb-board-cell--topics">—</div>
|
|
<div className="bb-board-cell bb-board-cell--posts">—</div>
|
|
<div className="bb-board-cell bb-board-cell--last">
|
|
<span className="bb-muted">{t('thread.no_replies')}</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
|
|
return (
|
|
<Container className="py-4 bb-portal-shell">
|
|
{loading && <p className="bb-muted">{t('home.loading')}</p>}
|
|
{error && <p className="text-danger">{error}</p>}
|
|
{!loading && forumTree.length === 0 && (
|
|
<p className="bb-muted">{t('home.empty')}</p>
|
|
)}
|
|
{forumTree.length > 0 && (
|
|
<div className="bb-board-index">
|
|
{forumTree.map((category) => (
|
|
<section className="bb-board-section" key={category.id}>
|
|
<header className="bb-board-section__header">
|
|
<span className="bb-board-section__title">{category.name}</span>
|
|
<div className="bb-board-section__controls">
|
|
<div className="bb-board-section__cols">
|
|
<span>{t('portal.topic')}</span>
|
|
<span>{t('thread.views')}</span>
|
|
<span>{t('thread.last_post')}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="bb-board-toggle"
|
|
onClick={() =>
|
|
setCollapsed((prev) => {
|
|
const next = {
|
|
...prev,
|
|
[category.id]: !prev[category.id],
|
|
}
|
|
const collapsedIds = Object.keys(next).filter((key) => next[key])
|
|
localStorage.setItem(storageKey, JSON.stringify(collapsedIds))
|
|
if (token) {
|
|
if (saveTimer.current) {
|
|
clearTimeout(saveTimer.current)
|
|
}
|
|
saveTimer.current = setTimeout(() => {
|
|
saveUserSetting(collapsedKey, collapsedIds).catch(() => {})
|
|
}, 400)
|
|
}
|
|
return next
|
|
})
|
|
}
|
|
aria-label={
|
|
collapsed[category.id]
|
|
? t('forum.expand_category')
|
|
: t('forum.collapse_category')
|
|
}
|
|
>
|
|
<i
|
|
className={`bi ${
|
|
collapsed[category.id] ? 'bi-plus-square' : 'bi-dash-square'
|
|
}`}
|
|
aria-hidden="true"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
{!collapsed[category.id] && (
|
|
<div className="bb-board-section__body">
|
|
{category.children?.length > 0 ? (
|
|
renderRows(category.children)
|
|
) : (
|
|
<div className="bb-board-empty">{t('forum.empty_children')}</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Container>
|
|
)
|
|
}
|