fixing thsu frontend views
This commit is contained in:
@@ -1061,7 +1061,7 @@ export default function Acp({ isAdmin }) {
|
||||
<div className="fw-semibold d-flex align-items-center gap-2">
|
||||
<span>{node.name}</span>
|
||||
</div>
|
||||
<div className="bb-muted">{node.description || t('forum.no_description')}</div>
|
||||
<div className="bb-muted">{node.description || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-3">
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function BoardIndex() {
|
||||
<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 className="bb-board-desc">{node.description || ''}</div>
|
||||
{node.children?.length > 0 && (
|
||||
<div className="bb-board-subforums">
|
||||
{t('forum.children')}:{' '}
|
||||
@@ -130,10 +130,28 @@ export default function BoardIndex() {
|
||||
</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--topics">{node.threads_count ?? 0}</div>
|
||||
<div className="bb-board-cell bb-board-cell--posts">{node.views_count ?? 0}</div>
|
||||
<div className="bb-board-cell bb-board-cell--last">
|
||||
<span className="bb-muted">{t('thread.no_replies')}</span>
|
||||
{node.last_post_at ? (
|
||||
<div className="bb-board-last">
|
||||
<span className="bb-board-last-by">
|
||||
{t('thread.by')}{' '}
|
||||
{node.last_post_user_id ? (
|
||||
<Link to={`/profile/${node.last_post_user_id}`} className="bb-board-last-link">
|
||||
{node.last_post_user_name || t('thread.anonymous')}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{node.last_post_user_name || t('thread.anonymous')}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="bb-board-last-date">
|
||||
{node.last_post_at.slice(0, 10)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="bb-muted">{t('thread.no_replies')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -31,14 +31,30 @@ export default function ForumView() {
|
||||
<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 className="bb-board-desc">{node.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--topics">{node.threads_count ?? 0}</div>
|
||||
<div className="bb-board-cell bb-board-cell--posts">{node.views_count ?? 0}</div>
|
||||
<div className="bb-board-cell bb-board-cell--last">
|
||||
<span className="bb-muted">{t('thread.no_replies')}</span>
|
||||
{node.last_post_at ? (
|
||||
<div className="bb-board-last">
|
||||
<span className="bb-board-last-by">
|
||||
{t('thread.by')}{' '}
|
||||
{node.last_post_user_id ? (
|
||||
<Link to={`/profile/${node.last_post_user_id}`} className="bb-board-last-link">
|
||||
{node.last_post_user_name || t('thread.anonymous')}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{node.last_post_user_name || t('thread.anonymous')}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="bb-board-last-date">{node.last_post_at.slice(0, 10)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="bb-muted">{t('thread.no_replies')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@@ -190,18 +206,33 @@ export default function ForumView() {
|
||||
</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--replies">
|
||||
{thread.posts_count ?? 0}
|
||||
</div>
|
||||
<div className="bb-topic-cell bb-topic-cell--views">
|
||||
{thread.views_count ?? 0}
|
||||
</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>
|
||||
{thread.last_post_user_id ? (
|
||||
<Link
|
||||
to={`/profile/${thread.last_post_user_id}`}
|
||||
className="bb-topic-author"
|
||||
>
|
||||
{thread.last_post_user_name || t('thread.anonymous')}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="bb-topic-author">
|
||||
{thread.last_post_user_name || t('thread.anonymous')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{thread.created_at && (
|
||||
<span className="bb-topic-date">{thread.created_at.slice(0, 10)}</span>
|
||||
{thread.last_post_at && (
|
||||
<span className="bb-topic-date">
|
||||
{thread.last_post_at.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Badge, Container } from 'react-bootstrap'
|
||||
import { Container } from 'react-bootstrap'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { getCurrentUser, listAllForums, listThreads } from '../api/client'
|
||||
import { fetchStats, getCurrentUser, listAllForums, listThreads } from '../api/client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
export default function Home() {
|
||||
const [forums, setForums] = useState([])
|
||||
const [threads, setThreads] = useState([])
|
||||
const [stats, setStats] = useState({ threads: 0, posts: 0, users: 0 })
|
||||
const [error, setError] = useState('')
|
||||
const [loadingForums, setLoadingForums] = useState(true)
|
||||
const [loadingThreads, setLoadingThreads] = useState(true)
|
||||
const [loadingStats, setLoadingStats] = useState(true)
|
||||
const [profile, setProfile] = useState(null)
|
||||
const { token, roles, email } = useAuth()
|
||||
const { t } = useTranslation()
|
||||
@@ -29,6 +31,21 @@ export default function Home() {
|
||||
.finally(() => setLoadingThreads(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats()
|
||||
.then((data) => {
|
||||
setStats({
|
||||
threads: data?.threads ?? 0,
|
||||
posts: data?.posts ?? 0,
|
||||
users: data?.users ?? 0,
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
setStats({ threads: 0, posts: 0, users: 0 })
|
||||
})
|
||||
.finally(() => setLoadingStats(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setProfile(null)
|
||||
@@ -110,6 +127,19 @@ export default function Home() {
|
||||
return t('portal.user_role_member')
|
||||
}, [roles, t])
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '—'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return '—'
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const year = String(date.getFullYear())
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
const resolveForumName = (thread) => {
|
||||
if (!thread?.forum) return t('portal.unknown_forum')
|
||||
const parts = thread.forum.split('/')
|
||||
@@ -138,7 +168,7 @@ export default function Home() {
|
||||
<Link to={`/forum/${node.id}`} className="bb-forum-link fw-semibold">
|
||||
{node.name}
|
||||
</Link>
|
||||
<div className="bb-muted">{node.description || t('forum.no_description')}</div>
|
||||
<div className="bb-muted">{node.description || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,11 +195,15 @@ export default function Home() {
|
||||
<div className="bb-portal-card-title">{t('portal.stats')}</div>
|
||||
<div className="bb-portal-stat">
|
||||
<span>{t('portal.stat_threads')}</span>
|
||||
<strong>{threads.length}</strong>
|
||||
<strong>{loadingStats ? '—' : stats.threads}</strong>
|
||||
</div>
|
||||
<div className="bb-portal-stat">
|
||||
<span>{t('portal.stat_forums')}</span>
|
||||
<strong>{forums.length}</strong>
|
||||
<span>{t('portal.stat_users')}</span>
|
||||
<strong>{loadingStats ? '—' : stats.users}</strong>
|
||||
</div>
|
||||
<div className="bb-portal-stat">
|
||||
<span>{t('portal.stat_posts')}</span>
|
||||
<strong>{loadingStats ? '—' : stats.posts}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -201,9 +235,18 @@ export default function Home() {
|
||||
</Link>
|
||||
<div className="bb-portal-topic-meta">
|
||||
<span>{t('thread.by')}</span>
|
||||
<Badge bg="secondary">
|
||||
{thread.user_name || t('thread.anonymous')}
|
||||
</Badge>
|
||||
{thread.user_id ? (
|
||||
<Link
|
||||
to={`/profile/${thread.user_id}`}
|
||||
className="bb-portal-topic-author"
|
||||
>
|
||||
{thread.user_name || t('thread.anonymous')}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="bb-portal-topic-author">
|
||||
{thread.user_name || t('thread.anonymous')}
|
||||
</span>
|
||||
)}
|
||||
<span className="bb-portal-topic-forum">
|
||||
{t('portal.forum_label')}{' '}
|
||||
{resolveForumId(thread) ? (
|
||||
@@ -220,10 +263,30 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bb-portal-topic-cell">0</div>
|
||||
<div className="bb-portal-topic-cell">—</div>
|
||||
<div className="bb-portal-topic-cell">{thread.posts_count ?? 0}</div>
|
||||
<div className="bb-portal-topic-cell">{thread.views_count ?? 0}</div>
|
||||
<div className="bb-portal-topic-cell">
|
||||
{thread.created_at?.slice(0, 10) || '—'}
|
||||
<div className="bb-portal-last">
|
||||
<span className="bb-portal-last-by">
|
||||
{t('thread.by')}{' '}
|
||||
{thread.last_post_user_id ? (
|
||||
<Link
|
||||
to={`/profile/${thread.last_post_user_id}`}
|
||||
className="bb-portal-last-link"
|
||||
>
|
||||
{thread.last_post_user_name || t('thread.anonymous')}
|
||||
<i className="bi bi-box-arrow-up-right" aria-hidden="true" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="bb-portal-last-link">
|
||||
{thread.last_post_user_name || t('thread.anonymous')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="bb-portal-last-date">
|
||||
{formatDateTime(thread.last_post_at || thread.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -64,6 +64,7 @@ export default function ThreadView() {
|
||||
user_avatar_url: thread.user_avatar_url,
|
||||
user_posts_count: thread.user_posts_count,
|
||||
user_created_at: thread.user_created_at,
|
||||
user_location: thread.user_location,
|
||||
user_rank_name: thread.user_rank_name,
|
||||
user_rank_badge_type: thread.user_rank_badge_type,
|
||||
user_rank_badge_text: thread.user_rank_badge_text,
|
||||
@@ -171,8 +172,10 @@ export default function ThreadView() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="bb-post-author-stat">
|
||||
<span className="bb-post-author-label">Location:</span>
|
||||
<span className="bb-post-author-value">Kollmar</span>
|
||||
<span className="bb-post-author-label">{t('thread.location')}:</span>
|
||||
<span className="bb-post-author-value">
|
||||
{post.user_location || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bb-post-author-stat">
|
||||
<span className="bb-post-author-label">Thanks given:</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Container, Form, Row, Col } from 'react-bootstrap'
|
||||
import { getCurrentUser, uploadAvatar } from '../api/client'
|
||||
import { Container, Form, Row, Col, Button } from 'react-bootstrap'
|
||||
import { getCurrentUser, updateCurrentUser, uploadAvatar } from '../api/client'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -11,6 +11,10 @@ export default function Ucp({ theme, setTheme, accentOverride, setAccentOverride
|
||||
const [avatarError, setAvatarError] = useState('')
|
||||
const [avatarUploading, setAvatarUploading] = useState(false)
|
||||
const [avatarPreview, setAvatarPreview] = useState('')
|
||||
const [location, setLocation] = useState('')
|
||||
const [profileError, setProfileError] = useState('')
|
||||
const [profileSaving, setProfileSaving] = useState(false)
|
||||
const [profileSaved, setProfileSaved] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
@@ -20,6 +24,7 @@ export default function Ucp({ theme, setTheme, accentOverride, setAccentOverride
|
||||
.then((data) => {
|
||||
if (!active) return
|
||||
setAvatarPreview(data?.avatar_url || '')
|
||||
setLocation(data?.location || '')
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setAvatarPreview('')
|
||||
@@ -76,6 +81,43 @@ export default function Ucp({ theme, setTheme, accentOverride, setAccentOverride
|
||||
/>
|
||||
<Form.Text className="bb-muted">{t('ucp.avatar_hint')}</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group className="mt-3">
|
||||
<Form.Label>{t('ucp.location_label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={location}
|
||||
disabled={!token || profileSaving}
|
||||
onChange={(event) => {
|
||||
setLocation(event.target.value)
|
||||
setProfileSaved(false)
|
||||
}}
|
||||
/>
|
||||
<Form.Text className="bb-muted">{t('ucp.location_hint')}</Form.Text>
|
||||
</Form.Group>
|
||||
{profileError && <p className="text-danger mt-2 mb-0">{profileError}</p>}
|
||||
{profileSaved && <p className="text-success mt-2 mb-0">{t('ucp.profile_saved')}</p>}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline-light"
|
||||
className="mt-3"
|
||||
disabled={!token || profileSaving}
|
||||
onClick={async () => {
|
||||
setProfileError('')
|
||||
setProfileSaved(false)
|
||||
setProfileSaving(true)
|
||||
try {
|
||||
const response = await updateCurrentUser({ location })
|
||||
setLocation(response?.location || '')
|
||||
setProfileSaved(true)
|
||||
} catch (err) {
|
||||
setProfileError(err.message)
|
||||
} finally {
|
||||
setProfileSaving(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{profileSaving ? t('form.saving') : t('ucp.save_profile')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user