Restyle thread view like phpBB

This commit is contained in:
Micha
2026-01-11 01:25:16 +01:00
parent eef3262a53
commit c8d2bd508e
3 changed files with 322 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Button, Container, Form } from 'react-bootstrap'
import { Link, useParams } from 'react-router-dom'
import { createPost, getThread, listPostsByThread } from '../api/client'
import { useAuth } from '../context/AuthContext'
@@ -15,6 +15,7 @@ export default function ThreadView() {
const [body, setBody] = useState('')
const [saving, setSaving] = useState(false)
const { t } = useTranslation()
const replyRef = useRef(null)
useEffect(() => {
setLoading(true)
@@ -43,70 +44,125 @@ export default function ThreadView() {
}
}
const replyCount = posts.length
const allPosts = useMemo(() => {
if (!thread) return posts
const rootPost = {
id: `thread-${thread.id}`,
body: thread.body,
created_at: thread.created_at,
user_name: thread.user_name,
isRoot: true,
}
return [rootPost, ...posts]
}, [posts, thread])
const handleJumpToReply = () => {
replyRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
const totalPosts = allPosts.length
return (
<Container className="py-5">
<Container className="py-4">
{loading && <p className="bb-muted">{t('thread.loading')}</p>}
{error && <p className="text-danger">{error}</p>}
{thread && (
<>
<div className="bb-hero mb-4">
<p className="bb-chip">{t('thread.label')}</p>
<h2 className="mt-3">{thread.title}</h2>
<p className="bb-muted mb-2">{thread.body}</p>
{thread.forum && (
<p className="bb-muted mb-0">
{t('thread.category')}{' '}
<Link to={`/forum/${thread.forum.id || thread.forum.split('/').pop()}`}>
{thread.forum.name || t('thread.back_to_category')}
</Link>
</p>
)}
<div className="bb-thread">
<div className="bb-thread-titlebar">
<h1 className="bb-thread-title">{thread.title}</h1>
<div className="bb-thread-meta">
<span>{t('thread.by')}</span>
<span className="bb-thread-author">
{thread.user_name || t('thread.anonymous')}
</span>
{thread.created_at && (
<span className="bb-thread-date">{thread.created_at.slice(0, 10)}</span>
)}
</div>
</div>
<Row className="g-4">
<Col lg={7}>
<h4 className="bb-section-title mb-3">{t('thread.replies')}</h4>
{posts.length === 0 && (
<p className="bb-muted">{t('thread.empty')}</p>
)}
{posts.map((post) => (
<Card className="bb-card mb-3" key={post.id}>
<Card.Body>
<Card.Text>{post.body}</Card.Text>
<small className="bb-muted">
{post.author?.username || t('thread.anonymous')}
</small>
</Card.Body>
</Card>
))}
</Col>
<Col lg={5}>
<h4 className="bb-section-title mb-3">{t('thread.reply')}</h4>
<div className="bb-form">
{!token && (
<p className="bb-muted mb-3">{t('thread.login_hint')}</p>
)}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>{t('form.message')}</Form.Label>
<Form.Control
as="textarea"
rows={5}
placeholder={t('form.reply_placeholder')}
value={body}
onChange={(event) => setBody(event.target.value)}
disabled={!token || saving}
required
/>
</Form.Group>
<Button type="submit" variant="dark" disabled={!token || saving}>
{saving ? t('form.posting') : t('form.post_reply')}
</Button>
</Form>
<div className="bb-thread-toolbar">
<div className="bb-thread-actions">
<Button className="bb-accent-button" onClick={handleJumpToReply}>
<i className="bi bi-reply-fill" aria-hidden="true" />
<span>{t('form.post_reply')}</span>
</Button>
<button type="button" className="bb-thread-icon-button" aria-label={t('thread.reply')}>
<i className="bi bi-arrow-counterclockwise" aria-hidden="true" />
</button>
<button type="button" className="bb-thread-icon-button" aria-label={t('thread.views')}>
<i className="bi bi-wrench" aria-hidden="true" />
</button>
<button type="button" className="bb-thread-icon-button" aria-label={t('thread.last_post')}>
<i className="bi bi-gear" aria-hidden="true" />
</button>
</div>
<div className="bb-thread-meta-right">
<span>{totalPosts} {totalPosts === 1 ? 'post' : 'posts'}</span>
<span></span>
<span>Page 1 of 1</span>
</div>
</div>
<div className="bb-posts">
{allPosts.map((post) => {
const authorName = post.author?.username
|| post.user_name
|| post.author_name
|| t('thread.anonymous')
return (
<article className="bb-post-row" key={post.id}>
<aside className="bb-post-author">
<div className="bb-post-avatar">
<i className="bi bi-person" aria-hidden="true" />
</div>
<div className="bb-post-author-name">{authorName}</div>
<div className="bb-post-author-meta">
{post.isRoot ? t('thread.label') : t('thread.reply')}
</div>
</aside>
<div className="bb-post-content">
<div className="bb-post-header">
<span>{t('thread.by')} {authorName}</span>
{post.created_at && (
<span>{post.created_at.slice(0, 10)}</span>
)}
</div>
<div className="bb-post-body">{post.body}</div>
</div>
</article>
)
})}
</div>
<div className="bb-thread-reply" ref={replyRef}>
<div className="bb-thread-reply-title">{t('thread.reply')}</div>
{!token && (
<p className="bb-muted mb-3">{t('thread.login_hint')}</p>
)}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label>{t('form.message')}</Form.Label>
<Form.Control
as="textarea"
rows={6}
placeholder={t('form.reply_placeholder')}
value={body}
onChange={(event) => setBody(event.target.value)}
disabled={!token || saving}
required
/>
</Form.Group>
<div className="bb-thread-reply-actions">
<Button type="submit" className="bb-accent-button" disabled={!token || saving}>
{saving ? t('form.posting') : t('form.post_reply')}
</Button>
</div>
</Col>
</Row>
</>
</Form>
</div>
</div>
)}
</Container>
)