UI: portal/header refinements, board index UX, and user settings
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'
|
||||
import { Button, Badge, Card, Col, Container, Form, Modal, Row } from 'react-bootstrap'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { createThread, getForum, listForumsByParent, listThreadsByForum } from '../api/client'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
@@ -13,11 +13,36 @@ export default function ForumView() {
|
||||
const [threads, setThreads] = useState([])
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [title, setTitle] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const renderChildRows = (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>
|
||||
</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>
|
||||
))
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
|
||||
@@ -62,6 +87,7 @@ export default function ForumView() {
|
||||
setBody('')
|
||||
const updated = await listThreadsByForum(id)
|
||||
setThreads(updated)
|
||||
setShowModal(false)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
@@ -75,106 +101,152 @@ export default function ForumView() {
|
||||
{error && <p className="text-danger">{error}</p>}
|
||||
{forum && (
|
||||
<>
|
||||
<div className="bb-hero mb-4">
|
||||
<p className="bb-chip">
|
||||
{forum.type === 'forum' ? t('forum.type_forum') : t('forum.type_category')}
|
||||
</p>
|
||||
<h2 className="mt-3">{forum.name}</h2>
|
||||
<p className="bb-muted mb-0">
|
||||
{forum.description || t('forum.no_description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Row className="g-4">
|
||||
<Col lg={7}>
|
||||
<h4 className="bb-section-title mb-3">{t('forum.children')}</h4>
|
||||
{children.length === 0 && (
|
||||
<p className="bb-muted">{t('forum.empty_children')}</p>
|
||||
<Col lg={12}>
|
||||
{forum.type !== 'forum' && (
|
||||
<div className="bb-board-index">
|
||||
<section className="bb-board-section">
|
||||
<header className="bb-board-section__header">
|
||||
<span className="bb-board-section__title">{forum.name}</span>
|
||||
<div className="bb-board-section__cols">
|
||||
<span>{t('portal.topic')}</span>
|
||||
<span>{t('thread.views')}</span>
|
||||
<span>{t('thread.last_post')}</span>
|
||||
</div>
|
||||
</header>
|
||||
<div className="bb-board-section__body">
|
||||
{children.length > 0 ? (
|
||||
renderChildRows(children)
|
||||
) : (
|
||||
<div className="bb-board-empty">{t('forum.empty_children')}</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
{children.map((child) => (
|
||||
<Card className="bb-card mb-3" key={child.id}>
|
||||
<Card.Body>
|
||||
<Card.Title>{child.name}</Card.Title>
|
||||
<Card.Text className="bb-muted">
|
||||
{child.description || t('forum.no_description')}
|
||||
</Card.Text>
|
||||
<Link to={`/forum/${child.id}`} className="stretched-link">
|
||||
{t('forum.open')}
|
||||
</Link>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{forum.type === 'forum' && (
|
||||
<>
|
||||
<h4 className="bb-section-title mb-3 mt-4">{t('forum.threads')}</h4>
|
||||
{threads.length === 0 && (
|
||||
<p className="bb-muted">{t('forum.empty_threads')}</p>
|
||||
)}
|
||||
{threads.map((thread) => (
|
||||
<Card className="bb-card mb-3" key={thread.id}>
|
||||
<Card.Body>
|
||||
<Card.Title>{thread.title}</Card.Title>
|
||||
<Card.Text className="bb-muted">
|
||||
{thread.body.length > 160
|
||||
? `${thread.body.slice(0, 160)}...`
|
||||
: thread.body}
|
||||
</Card.Text>
|
||||
<Link to={`/thread/${thread.id}`} className="stretched-link">
|
||||
{t('thread.view')}
|
||||
</Link>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
))}
|
||||
<div className="bb-topic-toolbar mt-4 mb-2">
|
||||
<div className="bb-topic-toolbar__left">
|
||||
<Button
|
||||
variant="dark"
|
||||
className="bb-topic-action bb-accent-button"
|
||||
onClick={() => setShowModal(true)}
|
||||
disabled={!token || saving}
|
||||
>
|
||||
<i className="bi bi-pencil me-2" aria-hidden="true" />
|
||||
{t('forum.start_thread')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="bb-topic-toolbar__right">
|
||||
<span className="bb-topic-count">
|
||||
{threads.length} {t('forum.threads').toLowerCase()}
|
||||
</span>
|
||||
<div className="bb-topic-pagination">
|
||||
<Button size="sm" variant="outline-secondary" disabled>
|
||||
‹
|
||||
</Button>
|
||||
<Button size="sm" variant="outline-secondary" className="is-active" disabled>
|
||||
1
|
||||
</Button>
|
||||
<Button size="sm" variant="outline-secondary" disabled>
|
||||
›
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!token && <p className="bb-muted mb-3">{t('forum.login_hint')}</p>}
|
||||
<div className="bb-topic-table">
|
||||
<div className="bb-topic-header">
|
||||
<div className="bb-topic-cell bb-topic-cell--title">{t('forum.threads')}</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--replies">{t('thread.replies')}</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--views">{t('thread.views')}</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--last">{t('thread.last_post')}</div>
|
||||
</div>
|
||||
{threads.length === 0 && (
|
||||
<div className="bb-topic-empty">{t('forum.empty_threads')}</div>
|
||||
)}
|
||||
{threads.map((thread) => (
|
||||
<div className="bb-topic-row" key={thread.id}>
|
||||
<div className="bb-topic-cell bb-topic-cell--title">
|
||||
<div className="bb-topic-title">
|
||||
<span className="bb-topic-icon" aria-hidden="true">
|
||||
<i className="bi bi-chat-left" />
|
||||
</span>
|
||||
<div className="bb-topic-text">
|
||||
<Link to={`/thread/${thread.id}`}>{thread.title}</Link>
|
||||
<div className="bb-topic-meta">
|
||||
<i className="bi bi-paperclip" aria-hidden="true" />
|
||||
<span>{t('thread.by')}</span>
|
||||
<span className="bb-topic-author">
|
||||
{thread.user_name || t('thread.anonymous')}
|
||||
</span>
|
||||
{thread.created_at && (
|
||||
<span className="bb-topic-date">
|
||||
{thread.created_at.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--replies">0</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--views">—</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--last">
|
||||
<span className="bb-muted">{t('thread.no_replies')}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
<Col lg={5}>
|
||||
<h4 className="bb-section-title mb-3">{t('forum.start_thread')}</h4>
|
||||
<div className="bb-form">
|
||||
{forum.type !== 'forum' && (
|
||||
<p className="bb-muted mb-3">{t('forum.only_forums')}</p>
|
||||
)}
|
||||
{forum.type === 'forum' && !token && (
|
||||
<p className="bb-muted mb-3">{t('forum.login_hint')}</p>
|
||||
)}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('form.title')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={t('form.thread_title_placeholder')}
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
disabled={!token || saving || forum.type !== 'forum'}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('form.body')}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={5}
|
||||
placeholder={t('form.thread_body_placeholder')}
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
disabled={!token || saving || forum.type !== 'forum'}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="dark"
|
||||
disabled={!token || saving || forum.type !== 'forum'}
|
||||
>
|
||||
{saving ? t('form.posting') : t('form.create_thread')}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
{forum?.type === 'forum' && (
|
||||
<Modal show={showModal} onHide={() => setShowModal(false)} centered size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('forum.start_thread')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{!token && <p className="bb-muted mb-3">{t('forum.login_hint')}</p>}
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('form.title')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder={t('form.thread_title_placeholder')}
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
disabled={!token || saving}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t('form.body')}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
rows={6}
|
||||
placeholder={t('form.thread_body_placeholder')}
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
disabled={!token || saving}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<div className="d-flex gap-2 justify-content-between">
|
||||
<Button type="button" variant="outline-secondary" onClick={() => setShowModal(false)}>
|
||||
{t('acp.cancel')}
|
||||
</Button>
|
||||
<Button type="submit" className="bb-accent-button" disabled={!token || saving}>
|
||||
{saving ? t('form.posting') : t('form.create_thread')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user