Files
speedBB/resources/js/pages/BoardIndex.jsx

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>
)
}