Files
speedBB/resources/js/pages/Ucp.jsx
2026-01-16 01:43:07 +01:00

180 lines
6.8 KiB
JavaScript

import { useEffect, useState } from 'react'
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'
export default function Ucp({ theme, setTheme, accentOverride, setAccentOverride }) {
const { t, i18n } = useTranslation()
const { token } = useAuth()
const accentMode = accentOverride ? 'custom' : 'system'
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
let active = true
getCurrentUser()
.then((data) => {
if (!active) return
setAvatarPreview(data?.avatar_url || '')
setLocation(data?.location || '')
})
.catch(() => {
if (active) setAvatarPreview('')
})
return () => {
active = false
}
}, [token])
const handleLanguageChange = (event) => {
const locale = event.target.value
i18n.changeLanguage(locale)
localStorage.setItem('speedbb_lang', locale)
}
return (
<Container fluid className="py-5 bb-portal-shell">
<div className="bb-portal-card mb-4">
<div className="bb-portal-card-title">{t('ucp.profile')}</div>
<p className="bb-muted mb-4">{t('ucp.profile_hint')}</p>
<Row className="g-3 align-items-center">
<Col md="auto">
<div className="bb-avatar-preview">
{avatarPreview ? (
<img src={avatarPreview} alt="" />
) : (
<i className="bi bi-person" aria-hidden="true" />
)}
</div>
</Col>
<Col>
{avatarError && <p className="text-danger mb-2">{avatarError}</p>}
<Form.Group>
<Form.Label>{t('ucp.avatar_label')}</Form.Label>
<Form.Control
type="file"
accept="image/png,image/jpeg,image/jpg,image/gif,image/webp"
disabled={!token || avatarUploading}
onChange={async (event) => {
const file = event.target.files?.[0]
if (!file) return
setAvatarError('')
setAvatarUploading(true)
try {
const response = await uploadAvatar(file)
setAvatarPreview(response.url)
} catch (err) {
setAvatarError(err.message)
} finally {
setAvatarUploading(false)
}
}}
/>
<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>
<div className="bb-portal-card">
<div className="bb-portal-card-title">{t('portal.user_control_panel')}</div>
<p className="bb-muted mb-4">{t('ucp.intro')}</p>
<Row className="g-3">
<Col xs={12}>
<Form.Group>
<Form.Label>{t('nav.language')}</Form.Label>
<Form.Select value={i18n.language} onChange={handleLanguageChange}>
<option value="en">English</option>
<option value="de">Deutsch</option>
</Form.Select>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group>
<Form.Label>{t('nav.theme')}</Form.Label>
<Form.Select value={theme} onChange={(event) => setTheme(event.target.value)}>
<option value="auto">{t('ucp.system_default')}</option>
<option value="dark">{t('nav.theme_dark')}</option>
<option value="light">{t('nav.theme_light')}</option>
</Form.Select>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group>
<Form.Label>{t('ucp.accent_override')}</Form.Label>
<div className="d-flex align-items-center gap-2">
<Form.Select
value={accentMode}
onChange={(event) => {
const mode = event.target.value
if (mode === 'system') {
setAccentOverride('')
} else if (!accentOverride) {
setAccentOverride('#f29b3f')
}
}}
>
<option value="system">{t('ucp.system_default')}</option>
<option value="custom">{t('ucp.custom_color')}</option>
</Form.Select>
<Form.Control
type="color"
value={accentOverride || '#f29b3f'}
onChange={(event) => setAccentOverride(event.target.value)}
disabled={accentMode !== 'custom'}
/>
</div>
<Form.Text className="bb-muted">{t('ucp.accent_override_hint')}</Form.Text>
</Form.Group>
</Col>
</Row>
</div>
</Container>
)
}