UI: portal/header refinements, board index UX, and user settings

This commit is contained in:
Micha
2026-01-01 19:54:02 +01:00
parent f83748cc76
commit 8604cdf95d
26 changed files with 2065 additions and 227 deletions

View File

@@ -1,20 +1,29 @@
import { useEffect, useMemo, useState } from 'react'
import { Container } from 'react-bootstrap'
import { Badge, Container } from 'react-bootstrap'
import { Link } from 'react-router-dom'
import { listAllForums } from '../api/client'
import { listAllForums, listThreads } from '../api/client'
import { useTranslation } from 'react-i18next'
export default function Home() {
const [forums, setForums] = useState([])
const [threads, setThreads] = useState([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [loadingForums, setLoadingForums] = useState(true)
const [loadingThreads, setLoadingThreads] = useState(true)
const { t } = useTranslation()
useEffect(() => {
listAllForums()
.then(setForums)
.catch((err) => setError(err.message))
.finally(() => setLoading(false))
.finally(() => setLoadingForums(false))
}, [])
useEffect(() => {
listThreads()
.then(setThreads)
.catch((err) => setError(err.message))
.finally(() => setLoadingThreads(false))
}, [])
const getParentId = (forum) => {
@@ -56,6 +65,33 @@ export default function Home() {
return roots
}, [forums])
const forumMap = useMemo(() => {
const map = new Map()
forums.forEach((forum) => {
map.set(String(forum.id), forum)
})
return map
}, [forums])
const recentThreads = useMemo(() => {
return [...threads]
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.slice(0, 12)
}, [threads])
const resolveForumName = (thread) => {
if (!thread?.forum) return t('portal.unknown_forum')
const parts = thread.forum.split('/')
const id = parts[parts.length - 1]
return forumMap.get(String(id))?.name || t('portal.unknown_forum')
}
const resolveForumId = (thread) => {
if (!thread?.forum) return null
const parts = thread.forum.split('/')
return parts[parts.length - 1] || null
}
const renderTree = (nodes, depth = 0) =>
nodes.map((node) => (
<div key={node.id}>
@@ -82,22 +118,111 @@ export default function Home() {
))
return (
<Container className="py-5">
<div className="bb-hero mb-4">
<p className="bb-chip">{t('app.brand')}</p>
<h1 className="mt-3">{t('home.hero_title')}</h1>
<p className="bb-muted mb-0">
{t('home.hero_body')}
</p>
</div>
<Container className="pb-4 bb-portal-shell">
<div className="bb-portal-layout">
<aside className="bb-portal-column bb-portal-column--left">
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('portal.menu')}</div>
<ul className="bb-portal-list">
<li>{t('portal.menu_news')}</li>
<li>{t('portal.menu_gallery')}</li>
<li>{t('portal.menu_calendar')}</li>
<li>{t('portal.menu_rules')}</li>
</ul>
</div>
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('portal.stats')}</div>
<div className="bb-portal-stat">
<span>{t('portal.stat_threads')}</span>
<strong>{threads.length}</strong>
</div>
<div className="bb-portal-stat">
<span>{t('portal.stat_forums')}</span>
<strong>{forums.length}</strong>
</div>
</div>
</aside>
<h3 className="bb-section-title mb-3">{t('home.browse')}</h3>
{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="mt-2">{renderTree(forumTree)}</div>}
<main className="bb-portal-column bb-portal-column--center">
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('portal.latest_posts')}</div>
{loadingThreads && <p className="bb-muted">{t('home.loading')}</p>}
{!loadingThreads && recentThreads.length === 0 && (
<p className="bb-muted">{t('portal.empty_posts')}</p>
)}
{!loadingThreads && recentThreads.length > 0 && (
<div className="bb-portal-topic-table">
<div className="bb-portal-topic-header">
<span>{t('portal.topic')}</span>
<span>{t('thread.replies')}</span>
<span>{t('thread.views')}</span>
<span>{t('thread.last_post')}</span>
</div>
{recentThreads.map((thread) => (
<div className="bb-portal-topic-row" key={thread.id}>
<div className="bb-portal-topic-main">
<span className="bb-portal-topic-icon" aria-hidden="true">
<i className="bi bi-chat-left-text" />
</span>
<div>
<Link to={`/thread/${thread.id}`} className="bb-portal-topic-title">
{thread.title}
</Link>
<div className="bb-portal-topic-meta">
<span>{t('thread.by')}</span>
<Badge bg="secondary">
{thread.user_name || t('thread.anonymous')}
</Badge>
<span className="bb-portal-topic-forum">
{t('portal.forum_label')}{' '}
{resolveForumId(thread) ? (
<Link
to={`/forum/${resolveForumId(thread)}`}
className="bb-portal-topic-forum-link"
>
{resolveForumName(thread)}
</Link>
) : (
resolveForumName(thread)
)}
</span>
</div>
</div>
</div>
<div className="bb-portal-topic-cell">0</div>
<div className="bb-portal-topic-cell"></div>
<div className="bb-portal-topic-cell">
{thread.created_at?.slice(0, 10) || '—'}
</div>
</div>
))}
</div>
)}
</div>
</main>
<aside className="bb-portal-column bb-portal-column--right">
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('portal.user_menu')}</div>
<div className="bb-portal-user-card">
<div className="bb-portal-user-avatar" />
<div className="bb-portal-user-name">tracer</div>
<div className="bb-portal-user-role">Operator</div>
</div>
<ul className="bb-portal-list">
<li>{t('portal.user_new_posts')}</li>
<li>{t('portal.user_unread')}</li>
<li>{t('portal.user_control_panel')}</li>
<li>{t('portal.user_logout')}</li>
</ul>
</div>
<div className="bb-portal-card bb-portal-card--ad">
<div className="bb-portal-card-title">{t('portal.advertisement')}</div>
<div className="bb-portal-ad-box">example.com</div>
</div>
</aside>
</div>
{error && <p className="text-danger mt-3">{error}</p>}
</Container>
)
}