Files
speedBB/resources/js/App.jsx
2025-12-29 18:19:24 +01:00

189 lines
6.0 KiB
JavaScript

import { useEffect, useState } from 'react'
import { BrowserRouter, Link, Route, Routes } from 'react-router-dom'
import { Container, Nav, Navbar, NavDropdown } from 'react-bootstrap'
import { AuthProvider, useAuth } from './context/AuthContext'
import Home from './pages/Home'
import ForumView from './pages/ForumView'
import ThreadView from './pages/ThreadView'
import Login from './pages/Login'
import Register from './pages/Register'
import Acp from './pages/Acp'
import { useTranslation } from 'react-i18next'
import { fetchSetting, fetchVersion } from './api/client'
function Navigation({ theme, onThemeChange }) {
const { token, email, logout, isAdmin } = useAuth()
const { t, i18n } = useTranslation()
const handleLanguageChange = (locale) => {
i18n.changeLanguage(locale)
localStorage.setItem('speedbb_lang', locale)
}
const handleThemeChange = (value) => {
onThemeChange(value)
localStorage.setItem('speedbb_theme', value)
}
return (
<Navbar expand="lg" className="bb-nav">
<Container>
<Navbar.Brand as={Link} to="/" className="fw-semibold">
{t('app.brand')}
</Navbar.Brand>
{isAdmin && (
<Nav className="me-auto">
<Nav.Link as={Link} to="/acp">
{t('nav.acp')}
</Nav.Link>
</Nav>
)}
<Navbar.Toggle aria-controls="bb-nav" />
<Navbar.Collapse id="bb-nav">
<Nav className="ms-auto align-items-lg-center gap-2">
{!token && (
<>
<Nav.Link as={Link} to="/login">
{t('nav.login')}
</Nav.Link>
<Nav.Link as={Link} to="/register">
{t('nav.register')}
</Nav.Link>
</>
)}
{token && (
<>
<span className="bb-chip">{email}</span>
<Nav.Link onClick={logout}>{t('nav.logout')}</Nav.Link>
</>
)}
<NavDropdown title={t('nav.language')} align="end">
<NavDropdown.Item onClick={() => handleLanguageChange('en')}>
English
</NavDropdown.Item>
<NavDropdown.Item onClick={() => handleLanguageChange('de')}>
Deutsch
</NavDropdown.Item>
</NavDropdown>
<NavDropdown title={t('nav.theme')} align="end">
<NavDropdown.Item onClick={() => handleThemeChange('auto')} active={theme === 'auto'}>
{t('nav.theme_auto')}
</NavDropdown.Item>
<NavDropdown.Item onClick={() => handleThemeChange('light')} active={theme === 'light'}>
{t('nav.theme_light')}
</NavDropdown.Item>
<NavDropdown.Item onClick={() => handleThemeChange('dark')} active={theme === 'dark'}>
{t('nav.theme_dark')}
</NavDropdown.Item>
</NavDropdown>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
)
}
function AppShell() {
const { t } = useTranslation()
const { isAdmin } = useAuth()
const [loadMs, setLoadMs] = useState(null)
const [versionInfo, setVersionInfo] = useState(null)
const [theme, setTheme] = useState(() => localStorage.getItem('speedbb_theme') || 'auto')
useEffect(() => {
const [entry] = performance.getEntriesByType('navigation')
if (entry?.duration) {
setLoadMs(Math.round(entry.duration))
return
}
setLoadMs(Math.round(performance.now()))
}, [])
useEffect(() => {
fetchVersion()
.then((data) => setVersionInfo(data))
.catch(() => setVersionInfo(null))
}, [])
useEffect(() => {
fetchSetting('accent_color')
.then((setting) => {
if (setting?.value) {
document.documentElement.style.setProperty('--bb-accent', setting.value)
}
})
.catch(() => {})
}, [])
useEffect(() => {
const root = document.documentElement
const media = window.matchMedia('(prefers-color-scheme: dark)')
const applyTheme = (mode) => {
if (mode === 'auto') {
root.setAttribute('data-bs-theme', media.matches ? 'dark' : 'light')
} else {
root.setAttribute('data-bs-theme', mode)
}
}
applyTheme(theme)
const handleChange = () => {
if (theme === 'auto') {
applyTheme('auto')
}
}
media.addEventListener('change', handleChange)
return () => {
media.removeEventListener('change', handleChange)
}
}, [theme])
return (
<div className="bb-shell">
<Navigation theme={theme} onThemeChange={setTheme} />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/forum/:id" element={<ForumView />} />
<Route path="/thread/:id" element={<ThreadView />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/acp/*" element={<Acp isAdmin={isAdmin} />} />
</Routes>
<footer className="bb-footer">
<div className="ms-3 d-flex align-items-center gap-3">
<span>{t('footer.copy')}</span>
{versionInfo?.version && (
<span className="bb-version">
<span className="bb-version-label">Version:</span>{' '}
<span className="bb-version-value">{versionInfo.version}</span>{' '}
<span className="bb-version-label">(build:</span>{' '}
<span className="bb-version-value">{versionInfo.build}</span>
<span className="bb-version-label">)</span>
</span>
)}
{loadMs !== null && (
<span className="bb-load-time">
<span className="bb-load-label">Page load time</span>{' '}
<span className="bb-load-value">{loadMs}ms</span>
</span>
)}
</div>
</footer>
</div>
)
}
export default function App() {
return (
<AuthProvider>
<BrowserRouter>
<AppShell />
</BrowserRouter>
</AuthProvider>
)
}