feat: add installer, ranks/groups enhancements, and founder protections

This commit is contained in:
2026-01-23 19:26:35 +01:00
parent 24c16ed0dd
commit d4fb86633b
43 changed files with 6176 additions and 4039 deletions

View File

@@ -2,64 +2,177 @@ import { useEffect, useState } from 'react'
import { Container } from 'react-bootstrap'
import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { getUserProfile } from '../api/client'
import { Link } from 'react-router-dom'
import { getUserProfile, listUserThanksGiven, listUserThanksReceived } from '../api/client'
export default function Profile() {
const { id } = useParams()
const { t } = useTranslation()
const [profile, setProfile] = useState(null)
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const { id } = useParams()
const { t } = useTranslation()
const [profile, setProfile] = useState(null)
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [thanksGiven, setThanksGiven] = useState([])
const [thanksReceived, setThanksReceived] = useState([])
const [loadingThanks, setLoadingThanks] = useState(true)
useEffect(() => {
let active = true
setLoading(true)
setError('')
useEffect(() => {
let active = true
setLoading(true)
setError('')
getUserProfile(id)
.then((data) => {
if (!active) return
setProfile(data)
})
.catch((err) => {
if (!active) return
setError(err.message)
})
.finally(() => {
if (active) setLoading(false)
})
Promise.all([getUserProfile(id), listUserThanksGiven(id), listUserThanksReceived(id)])
.then(([profileData, givenData, receivedData]) => {
if (!active) return
setProfile(profileData)
setThanksGiven(Array.isArray(givenData) ? givenData : [])
setThanksReceived(Array.isArray(receivedData) ? receivedData : [])
})
.catch((err) => {
if (!active) return
setError(err.message)
setThanksGiven([])
setThanksReceived([])
})
.finally(() => {
if (!active) return
setLoading(false)
setLoadingThanks(false)
})
return () => {
active = false
return () => {
active = false
}
}, [id])
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}`
}
}, [id])
return (
<Container fluid className="py-5 bb-portal-shell">
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('profile.title')}</div>
{loading && <p className="bb-muted">{t('profile.loading')}</p>}
{error && <p className="text-danger">{error}</p>}
{profile && (
<div className="bb-profile">
<div className="bb-profile-avatar">
{profile.avatar_url ? (
<img src={profile.avatar_url} alt="" />
) : (
<i className="bi bi-person" aria-hidden="true" />
)}
return (
<Container fluid className="py-5 bb-portal-shell">
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('profile.title')}</div>
{loading && <p className="bb-muted">{t('profile.loading')}</p>}
{error && <p className="text-danger">{error}</p>}
{profile && (
<div className="bb-profile">
<div className="bb-profile-avatar">
{profile.avatar_url ? (
<img src={profile.avatar_url} alt="" />
) : (
<i className="bi bi-person" aria-hidden="true" />
)}
</div>
<div className="bb-profile-meta">
<div className="bb-profile-name">{profile.name}</div>
{profile.created_at && (
<div className="bb-muted">
{t('profile.registered')} {profile.created_at.slice(0, 10)}
</div>
)}
</div>
</div>
)}
{profile && (
<div className="bb-profile-thanks mt-4">
<div className="bb-profile-section">
<div className="bb-portal-card-title">{t('profile.thanks_given')}</div>
{loadingThanks && <p className="bb-muted">{t('profile.loading')}</p>}
{!loadingThanks && thanksGiven.length === 0 && (
<p className="bb-muted">{t('profile.thanks_empty')}</p>
)}
{!loadingThanks && thanksGiven.length > 0 && (
<ul className="bb-profile-thanks-list">
{thanksGiven.map((item) => (
<li key={item.id} className="bb-profile-thanks-item">
<Link to={`/thread/${item.thread_id}#post-${item.post_id}`}>
{item.thread_title || t('thread.label')}
</Link>
{item.post_author_id ? (
<span className="bb-profile-thanks-meta">
{t('profile.thanks_for')}{' '}
<Link
to={`/profile/${item.post_author_id}`}
style={
item.post_author_rank_color || item.post_author_group_color
? {
'--bb-user-link-color':
item.post_author_rank_color || item.post_author_group_color,
}
: undefined
}
>
{item.post_author_name || t('thread.anonymous')}
</Link>
</span>
) : (
<span className="bb-profile-thanks-meta">
{t('profile.thanks_for')} {item.post_author_name || t('thread.anonymous')}
</span>
)}
<span className="bb-profile-thanks-date">
{formatDateTime(item.thanked_at)}
</span>
</li>
))}
</ul>
)}
</div>
<div className="bb-profile-section">
<div className="bb-portal-card-title">{t('profile.thanks_received')}</div>
{loadingThanks && <p className="bb-muted">{t('profile.loading')}</p>}
{!loadingThanks && thanksReceived.length === 0 && (
<p className="bb-muted">{t('profile.thanks_empty')}</p>
)}
{!loadingThanks && thanksReceived.length > 0 && (
<ul className="bb-profile-thanks-list">
{thanksReceived.map((item) => (
<li key={item.id} className="bb-profile-thanks-item">
<Link to={`/thread/${item.thread_id}#post-${item.post_id}`}>
{item.thread_title || t('thread.label')}
</Link>
{item.thanker_id ? (
<span className="bb-profile-thanks-meta">
{t('profile.thanks_by')}{' '}
<Link
to={`/profile/${item.thanker_id}`}
style={
item.thanker_rank_color || item.thanker_group_color
? {
'--bb-user-link-color':
item.thanker_rank_color || item.thanker_group_color,
}
: undefined
}
>
{item.thanker_name || t('thread.anonymous')}
</Link>
</span>
) : (
<span className="bb-profile-thanks-meta">
{t('profile.thanks_by')} {item.thanker_name || t('thread.anonymous')}
</span>
)}
<span className="bb-profile-thanks-date">
{formatDateTime(item.thanked_at)}
</span>
</li>
))}
</ul>
)}
</div>
</div>
)}
</div>
<div className="bb-profile-meta">
<div className="bb-profile-name">{profile.name}</div>
{profile.created_at && (
<div className="bb-muted">
{t('profile.registered')} {profile.created_at.slice(0, 10)}
</div>
)}
</div>
</div>
)}
</div>
</Container>
)
</Container>
)
}