From c8d2bd508e762c40c4f39e46e3a4ea3a60715841 Mon Sep 17 00:00:00 2001 From: Micha Date: Sun, 11 Jan 2026 01:25:16 +0100 Subject: [PATCH] Restyle thread view like phpBB --- CHANGELOG.md | 3 + resources/js/index.css | 204 ++++++++++++++++++++++++++++++ resources/js/pages/ThreadView.jsx | 174 ++++++++++++++++--------- 3 files changed, 322 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd47af..14ed2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ - Added admin-only upload endpoints and ACP UI for logos and favicons. - Applied forum branding, theme defaults, accents, logos, and favicon links in the SPA header. +## 2026-01-11 +- Restyled the thread view to mimic phpBB: compact toolbar, title row, and post layout. + ## 2025-12-30 - Added soft deletes with audit metadata (deleted_at/deleted_by) for forums, threads, and posts. - Ensured API listings and ACP forum tree omit soft-deleted records by default. diff --git a/resources/js/index.css b/resources/js/index.css index 6cdebea..bc9292b 100644 --- a/resources/js/index.css +++ b/resources/js/index.css @@ -88,6 +88,210 @@ a { padding: 1.2rem; } +.bb-thread { + display: flex; + flex-direction: column; + gap: 1.4rem; +} + +.bb-thread-titlebar { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.bb-thread-title { + margin: 0; + font-size: 1.6rem; + color: var(--bb-accent, #f29b3f); +} + +.bb-thread-meta { + display: flex; + align-items: center; + gap: 0.6rem; + color: var(--bb-ink-muted); + font-size: 0.95rem; +} + +.bb-thread-author { + color: var(--bb-accent, #f29b3f); + font-weight: 600; +} + +.bb-thread-date { + opacity: 0.8; +} + +.bb-thread-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.5rem 0.8rem; + border-radius: 10px; + border: 1px solid var(--bb-border); + background: #141822; + flex-wrap: wrap; +} + +.bb-thread-actions { + display: flex; + align-items: center; + gap: 0.4rem; + flex-wrap: wrap; +} + +.bb-thread-toolbar .bb-accent-button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + padding: 0.35rem 0.75rem; + border-radius: 8px; + font-size: 0.9rem; +} + +.bb-thread-icon-button { + border: 1px solid #2a2f3a; + background: #20252f; + color: #d0d6df; + width: 32px; + height: 32px; + border-radius: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + transition: border-color 0.15s ease, color 0.15s ease; +} + +.bb-thread-icon-button:hover { + color: var(--bb-accent, #f29b3f); + border-color: var(--bb-accent, #f29b3f); +} + +.bb-thread-meta-right { + display: flex; + align-items: center; + gap: 0.35rem; + color: var(--bb-ink-muted); + font-size: 0.85rem; +} + +.bb-thread-stats { + display: flex; + align-items: center; + gap: 0.4rem; + color: var(--bb-ink-muted); + font-size: 0.9rem; +} + +.bb-thread-empty { + color: var(--bb-accent, #f29b3f); + margin-left: 0.6rem; +} + +.bb-posts { + border: 1px solid var(--bb-border); + border-radius: 16px; + overflow: hidden; + background: #171b22; +} + +.bb-post-row { + display: grid; + grid-template-columns: 220px 1fr; + border-top: 1px solid var(--bb-border); +} + +.bb-post-row:first-child { + border-top: 0; +} + +.bb-post-author { + padding: 1rem; + background: rgba(255, 255, 255, 0.02); + border-right: 1px solid var(--bb-border); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.bb-post-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.06); + color: var(--bb-accent, #f29b3f); + font-size: 1.1rem; +} + +.bb-post-author-name { + font-weight: 600; + color: var(--bb-accent, #f29b3f); +} + +.bb-post-author-meta { + font-size: 0.85rem; + color: var(--bb-ink-muted); +} + +.bb-post-content { + padding: 1rem 1.35rem 1.2rem; +} + +.bb-post-header { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.85rem; + color: var(--bb-ink-muted); + margin-bottom: 0.75rem; +} + +.bb-post-body { + white-space: pre-wrap; + color: var(--bb-ink); + line-height: 1.6; +} + +.bb-thread-reply { + border: 1px solid var(--bb-border); + border-radius: 16px; + padding: 1.1rem 1.2rem 1.4rem; + background: #171b22; +} + +.bb-thread-reply-title { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--bb-accent, #f29b3f); + margin-bottom: 1rem; + font-size: 0.95rem; +} + +.bb-thread-reply-actions { + display: flex; + justify-content: flex-end; +} + +@media (max-width: 992px) { + .bb-post-row { + grid-template-columns: 1fr; + } + + .bb-post-author { + border-right: 0; + border-bottom: 1px solid var(--bb-border); + flex-direction: row; + align-items: center; + } +} + .bb-forum-row { background: rgba(255, 255, 255, 0.04); transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; diff --git a/resources/js/pages/ThreadView.jsx b/resources/js/pages/ThreadView.jsx index 147636c..1f57702 100644 --- a/resources/js/pages/ThreadView.jsx +++ b/resources/js/pages/ThreadView.jsx @@ -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 ( - + {loading &&

{t('thread.loading')}

} {error &&

{error}

} {thread && ( - <> -
-

{t('thread.label')}

-

{thread.title}

-

{thread.body}

- {thread.forum && ( -

- {t('thread.category')}{' '} - - {thread.forum.name || t('thread.back_to_category')} - -

- )} +
+
+

{thread.title}

+
+ {t('thread.by')} + + {thread.user_name || t('thread.anonymous')} + + {thread.created_at && ( + {thread.created_at.slice(0, 10)} + )} +
- - -

{t('thread.replies')}

- {posts.length === 0 && ( -

{t('thread.empty')}

- )} - {posts.map((post) => ( - - - {post.body} - - {post.author?.username || t('thread.anonymous')} - - - - ))} - - -

{t('thread.reply')}

-
- {!token && ( -

{t('thread.login_hint')}

- )} -
- - {t('form.message')} - setBody(event.target.value)} - disabled={!token || saving} - required - /> - - -
+
+
+