189 lines
6.0 KiB
JavaScript
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>
|
|
)
|
|
}
|