feat: add solved threads
This commit is contained in:
@@ -246,6 +246,16 @@ export async function getThread(id) {
|
||||
return apiFetch(`/threads/${id}`)
|
||||
}
|
||||
|
||||
export async function updateThreadSolved(threadId, solved) {
|
||||
return apiFetch(`/threads/${threadId}/solved`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
body: JSON.stringify({ solved }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function listPostsByThread(threadId) {
|
||||
return getCollection(`/posts?thread=/api/threads/${threadId}`)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ export default function PortalTopicRow({ thread, forumName, forumId, showForum =
|
||||
<div>
|
||||
<Link to={`/thread/${thread.id}`} className="bb-portal-topic-title">
|
||||
{thread.title}
|
||||
{thread.solved && (
|
||||
<span className="bb-thread-solved-badge">{t('thread.solved')}</span>
|
||||
)}
|
||||
</Link>
|
||||
<div className="bb-portal-topic-meta">
|
||||
<div className="bb-portal-topic-meta-line">
|
||||
|
||||
@@ -105,6 +105,40 @@ a {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
color: var(--bb-accent, #f29b3f);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bb-thread-solved-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.18rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: var(--bb-accent, #f29b3f);
|
||||
color: #0b0f17;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
margin-left: 0.45rem;
|
||||
}
|
||||
|
||||
.bb-thread-solved-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
color: var(--bb-accent, #f29b3f);
|
||||
border-color: var(--bb-accent, #f29b3f);
|
||||
}
|
||||
|
||||
.bb-thread-solved-toggle:hover,
|
||||
.bb-thread-solved-toggle:focus {
|
||||
background: var(--bb-accent, #f29b3f);
|
||||
border-color: var(--bb-accent, #f29b3f);
|
||||
color: #0b0f17;
|
||||
}
|
||||
|
||||
.bb-thread-meta {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Button, Container, Form } from 'react-bootstrap'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { createPost, getThread, listPostsByThread } from '../api/client'
|
||||
import { createPost, getThread, listPostsByThread, updateThreadSolved } from '../api/client'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function ThreadView() {
|
||||
const { id } = useParams()
|
||||
const { token, userId } = useAuth()
|
||||
const { token, userId, isAdmin } = useAuth()
|
||||
const [thread, setThread] = useState(null)
|
||||
const [posts, setPosts] = useState([])
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [body, setBody] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [solving, setSolving] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const replyRef = useRef(null)
|
||||
|
||||
@@ -93,6 +94,24 @@ export default function ThreadView() {
|
||||
replyRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
const canToggleSolved = token
|
||||
&& thread
|
||||
&& (Number(thread.user_id) === Number(userId) || isAdmin)
|
||||
|
||||
const handleToggleSolved = async () => {
|
||||
if (!thread || solving) return
|
||||
setSolving(true)
|
||||
setError('')
|
||||
try {
|
||||
const updated = await updateThreadSolved(thread.id, !thread.solved)
|
||||
setThread(updated)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setSolving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const totalPosts = allPosts.length
|
||||
|
||||
return (
|
||||
@@ -102,7 +121,12 @@ export default function ThreadView() {
|
||||
{thread && (
|
||||
<div className="bb-thread">
|
||||
<div className="bb-thread-titlebar">
|
||||
<h1 className="bb-thread-title">{thread.title}</h1>
|
||||
<h1 className="bb-thread-title">
|
||||
{thread.title}
|
||||
{thread.solved && (
|
||||
<span className="bb-thread-solved-badge">{t('thread.solved')}</span>
|
||||
)}
|
||||
</h1>
|
||||
<div className="bb-thread-meta">
|
||||
<span>{t('thread.by')}</span>
|
||||
<span className="bb-thread-author">
|
||||
@@ -120,6 +144,20 @@ export default function ThreadView() {
|
||||
<i className="bi bi-reply-fill" aria-hidden="true" />
|
||||
<span>{t('form.post_reply')}</span>
|
||||
</Button>
|
||||
{canToggleSolved && (
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
className="bb-thread-solved-toggle"
|
||||
onClick={handleToggleSolved}
|
||||
disabled={solving}
|
||||
>
|
||||
<i
|
||||
className={`bi ${thread.solved ? 'bi-check-circle-fill' : 'bi-check-circle'}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{thread.solved ? t('thread.mark_unsolved') : t('thread.mark_solved')}</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>
|
||||
|
||||
@@ -227,6 +227,9 @@
|
||||
"thread.reply_prefix": "Aw:",
|
||||
"thread.registered": "Registriert",
|
||||
"thread.replies": "Antworten",
|
||||
"thread.solved": "Gel\u00f6st",
|
||||
"thread.mark_solved": "Als gel\u00f6st markieren",
|
||||
"thread.mark_unsolved": "Als ungel\u00f6st markieren",
|
||||
"thread.views": "Zugriffe",
|
||||
"thread.last_post": "Letzter Beitrag",
|
||||
"thread.by": "von",
|
||||
|
||||
@@ -227,6 +227,9 @@
|
||||
"thread.reply_prefix": "Re:",
|
||||
"thread.registered": "Registered",
|
||||
"thread.replies": "Replies",
|
||||
"thread.solved": "Solved",
|
||||
"thread.mark_solved": "Mark solved",
|
||||
"thread.mark_unsolved": "Mark unsolved",
|
||||
"thread.views": "Views",
|
||||
"thread.last_post": "Last post",
|
||||
"thread.by": "by",
|
||||
|
||||
Reference in New Issue
Block a user