263 lines
11 KiB
JavaScript
263 lines
11 KiB
JavaScript
import { useEffect, useState } from 'react'
|
||
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'
|
||
import { useTranslation } from 'react-i18next'
|
||
|
||
export default function ForumView() {
|
||
const { id } = useParams()
|
||
const { token } = useAuth()
|
||
const [forum, setForum] = useState(null)
|
||
const [children, setChildren] = useState([])
|
||
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
|
||
|
||
const loadData = async () => {
|
||
setLoading(true)
|
||
setError('')
|
||
try {
|
||
const forumData = await getForum(id)
|
||
if (!active) return
|
||
setForum(forumData)
|
||
const childData = await listForumsByParent(id)
|
||
if (!active) return
|
||
setChildren(childData)
|
||
if (forumData.type === 'forum') {
|
||
const threadData = await listThreadsByForum(id)
|
||
if (!active) return
|
||
setThreads(threadData)
|
||
} else {
|
||
setThreads([])
|
||
}
|
||
} catch (err) {
|
||
if (active) setError(err.message)
|
||
} finally {
|
||
if (active) setLoading(false)
|
||
}
|
||
}
|
||
|
||
loadData()
|
||
|
||
return () => {
|
||
active = false
|
||
}
|
||
}, [id])
|
||
|
||
const handleSubmit = async (event) => {
|
||
event.preventDefault()
|
||
setSaving(true)
|
||
setError('')
|
||
try {
|
||
await createThread({ title, body, forumId: id })
|
||
setTitle('')
|
||
setBody('')
|
||
const updated = await listThreadsByForum(id)
|
||
setThreads(updated)
|
||
setShowModal(false)
|
||
} catch (err) {
|
||
setError(err.message)
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Container className="py-5">
|
||
{loading && <p className="bb-muted">{t('forum.loading')}</p>}
|
||
{error && <p className="text-danger">{error}</p>}
|
||
{forum && (
|
||
<>
|
||
<Row className="g-4">
|
||
<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>
|
||
)}
|
||
{forum.type === 'forum' && (
|
||
<>
|
||
<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">
|
||
<div className="bb-topic-last">
|
||
<span className="bb-topic-last-by">
|
||
{t('thread.by')}{' '}
|
||
<span className="bb-topic-author">
|
||
{thread.user_name || t('thread.anonymous')}
|
||
</span>
|
||
</span>
|
||
{thread.created_at && (
|
||
<span className="bb-topic-date">{thread.created_at.slice(0, 10)}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</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>
|
||
)
|
||
}
|