refactor: connect frontend to backend by integrating SettingsContext and updating route handling for forum and bug reporting features

This commit is contained in:
Thibault Pouch
2026-03-17 16:44:06 +01:00
parent a46dfde6d2
commit 792816c6c8
4 changed files with 48 additions and 70 deletions

View File

@@ -1,6 +1,7 @@
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext'; import { AuthProvider } from './contexts/AuthContext';
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
import { ProtectedRoute } from './components/shared/ProtectedRoute'; import { ProtectedRoute } from './components/shared/ProtectedRoute';
import { PublicLayout } from './components/layout/PublicLayout'; import { PublicLayout } from './components/layout/PublicLayout';
import { PageLoader } from './components/shared/PageLoader'; import { PageLoader } from './components/shared/PageLoader';
@@ -19,22 +20,24 @@ const LoginPage = lazy(() => import('./pages/public/LoginPage'));
const RegisterPage = lazy(() => import('./pages/public/RegisterPage')); const RegisterPage = lazy(() => import('./pages/public/RegisterPage'));
const NotFoundPage = lazy(() => import('./pages/public/NotFoundPage')); const NotFoundPage = lazy(() => import('./pages/public/NotFoundPage'));
// ── App ──────────────────────────────────────────────────────────────────────── // ── Routes (needs SettingsContext) ────────────────────────────────────────────
function AppRoutes() {
const { forumEnabled, bugsEnabled, loaded } = useSettings();
if (!loaded) return <PageLoader />;
export default function App() {
return ( return (
<AuthProvider>
<Suspense fallback={<PageLoader />}> <Suspense fallback={<PageLoader />}>
<Routes> <Routes>
{/* Public Routes */}
<Route element={<PublicLayout />}> <Route element={<PublicLayout />}>
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
<Route path="studio" element={<StudioPage />} /> <Route path="studio" element={<StudioPage />} />
<Route path="events" element={<EventsPage />} /> <Route path="events" element={<EventsPage />} />
<Route path="forum" element={<ForumPage />} /> <Route path="forum" element={forumEnabled ? <ForumPage /> : <NotFoundPage />} />
<Route path="forum/thread/:id" element={<ThreadPage />} /> <Route path="forum/thread/:id" element={forumEnabled ? <ThreadPage /> : <NotFoundPage />} />
<Route path="bugs" element={<BugReportPage />} /> <Route path="bugs" element={bugsEnabled ? <BugReportPage /> : <NotFoundPage />} />
<Route path="bugs/:id" element={<BugDetailPage />} /> <Route path="bugs/:id" element={bugsEnabled ? <BugDetailPage /> : <NotFoundPage />} />
<Route <Route
path="account" path="account"
element={ element={
@@ -49,6 +52,17 @@ export default function App() {
</Route> </Route>
</Routes> </Routes>
</Suspense> </Suspense>
);
}
// ── App ────────────────────────────────────────────────────────────────────────
export default function App() {
return (
<AuthProvider>
<SettingsProvider>
<AppRoutes />
</SettingsProvider>
</AuthProvider> </AuthProvider>
); );
} }

View File

@@ -88,7 +88,7 @@ export default function BugDetailPage() {
}, [id]); }, [id]);
const alreadyVoted = useMemo( const alreadyVoted = useMemo(
() => !!user && !!bug && bug.meTooBugs.includes(user.id), () => !!user && !!bug && (bug.meTooBugs ?? []).includes(user.id),
[user, bug] [user, bug]
); );
const isOwnReport = useMemo( const isOwnReport = useMemo(
@@ -100,7 +100,7 @@ export default function BugDetailPage() {
if (!user || !bug || alreadyVoted || isOwnReport) return; if (!user || !bug || alreadyVoted || isOwnReport) return;
try { try {
await bugsApi.toggleMeToo(bug.id); await bugsApi.toggleMeToo(bug.id);
setBug((prev) => prev ? { ...prev, meTooBugs: [...prev.meTooBugs, user.id] } : prev); setBug((prev) => prev ? { ...prev, meTooBugs: [...(prev.meTooBugs ?? []), user.id] } : prev);
} catch { } catch {
// silently ignore // silently ignore
} }
@@ -137,7 +137,7 @@ export default function BugDetailPage() {
return <Navigate to="/bugs" replace />; return <Navigate to="/bugs" replace />;
} }
const metooCount = bug.meTooBugs.length; const metooCount = (bug.meTooBugs ?? []).length;
return ( return (
<div style={{ maxWidth: '860px', margin: '0 auto', padding: '3rem 1.5rem' }}> <div style={{ maxWidth: '860px', margin: '0 auto', padding: '3rem 1.5rem' }}>

View File

@@ -1,6 +1,6 @@
import { useState, useMemo, useCallback, useEffect } from 'react'; import { useState, useMemo, useCallback, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { bugsApi, settingsApi } from '../../utils/api'; import { bugsApi } from '../../utils/api';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { timeAgo } from '../../utils/format'; import { timeAgo } from '../../utils/format';
import type { BugReport, BugSeverity, BugStatus, BugReportFormData } from '../../types'; import type { BugReport, BugSeverity, BugStatus, BugReportFormData } from '../../types';
@@ -78,7 +78,7 @@ function BugCard({ bug, highlight }: BugCardProps) {
borderRadius: '3px', borderRadius: '3px',
}} }}
> >
&#9654; {bug.meTooBugs.length} {bug.meTooBugs.length === 1 ? 'user' : 'users'} have this &#9654; {(bug.meTooBugs ?? []).length} {(bug.meTooBugs ?? []).length === 1 ? 'user' : 'users'} have this
</span> </span>
</div> </div>
@@ -278,19 +278,14 @@ export default function BugReportPage() {
const { user, isAuthenticated } = useAuth(); const { user, isAuthenticated } = useAuth();
const [bugs, setBugs] = useState<BugReport[]>([]); const [bugs, setBugs] = useState<BugReport[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [bugsEnabled, setBugsEnabled] = useState(true);
const [statusFilter, setStatusFilter] = useState<BugStatus | 'all'>('all'); const [statusFilter, setStatusFilter] = useState<BugStatus | 'all'>('all');
const [severityFilter, setSeverityFilter] = useState<BugSeverity | 'all'>('all'); const [severityFilter, setSeverityFilter] = useState<BugSeverity | 'all'>('all');
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
useEffect(() => {
settingsApi.get().then((s) => setBugsEnabled(s.bugsEnabled)).catch(() => {});
}, []);
const fetchBugs = useCallback(() => { const fetchBugs = useCallback(() => {
setLoading(true); setLoading(true);
bugsApi.getBugs({ status: statusFilter, severity: severityFilter, limit: 100 }) bugsApi.getBugs({ status: statusFilter, severity: severityFilter, limit: 100 })
.then((res) => setBugs(res.data)) .then((res) => setBugs(Array.isArray(res?.data) ? res.data : []))
.catch(() => setBugs([])) .catch(() => setBugs([]))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [statusFilter, severityFilter]); }, [statusFilter, severityFilter]);
@@ -300,7 +295,7 @@ export default function BugReportPage() {
const { myBugs, otherBugs } = useMemo(() => { const { myBugs, otherBugs } = useMemo(() => {
const my: BugReport[] = []; const my: BugReport[] = [];
const other: BugReport[] = []; const other: BugReport[] = [];
bugs.forEach((b) => { (bugs ?? []).forEach((b) => {
if (user && b.submittedById === user.id) my.push(b); if (user && b.submittedById === user.id) my.push(b);
else other.push(b); else other.push(b);
}); });
@@ -317,20 +312,6 @@ export default function BugReportPage() {
const inProgressCount = bugs.filter((b) => b.status === 'in_progress').length; const inProgressCount = bugs.filter((b) => b.status === 'in_progress').length;
const resolvedCount = bugs.filter((b) => b.status === 'resolved').length; const resolvedCount = bugs.filter((b) => b.status === 'resolved').length;
if (!bugsEnabled) {
return (
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '3rem 1.5rem', textAlign: 'center' }}>
<div className="section-label">Issue Tracker</div>
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-red)', fontSize: 'clamp(2rem, 6vw, 3.5rem)', marginTop: '0.25rem' }}>
BUG REPORTS UNAVAILABLE
</h1>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.78rem', marginTop: '1rem', fontFamily: 'var(--font-mono)' }}>
Bug reporting has been temporarily disabled by an administrator.
</p>
</div>
);
}
return ( return (
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '3rem 1.5rem' }}> <div style={{ maxWidth: '960px', margin: '0 auto', padding: '3rem 1.5rem' }}>
{/* Header */} {/* Header */}

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { forumApi, settingsApi } from '../../utils/api'; import { forumApi } from '../../utils/api';
import { timeAgo } from '../../utils/format'; import { timeAgo } from '../../utils/format';
import type { ForumCategory, ForumThread } from '../../types'; import type { ForumCategory, ForumThread } from '../../types';
@@ -132,20 +132,17 @@ export default function ForumPage() {
const [threads, setThreads] = useState<ForumThread[]>([]); const [threads, setThreads] = useState<ForumThread[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [forumEnabled, setForumEnabled] = useState(true);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
setLoading(true); setLoading(true);
Promise.all([ Promise.all([
settingsApi.get(),
forumApi.getCategories(), forumApi.getCategories(),
forumApi.getThreads({ limit: 200 }), forumApi.getThreads({ limit: 200 }),
]) ])
.then(([settings, cats, threadRes]) => { .then(([cats, threadRes]) => {
if (cancelled) return; if (cancelled) return;
setForumEnabled(settings.forumEnabled);
setCategories(cats); setCategories(cats);
setThreads(threadRes.data); setThreads(threadRes.data);
}) })
@@ -169,20 +166,6 @@ export default function ForumPage() {
); );
}, [search, categories, threads]); }, [search, categories, threads]);
if (!loading && !forumEnabled) {
return (
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '4rem 1.5rem', textAlign: 'center' }}>
<div className="section-label">Community</div>
<h1 style={{ fontFamily: 'var(--font-heading)', color: 'var(--color-red)', fontSize: 'clamp(2rem, 5vw, 3rem)', marginTop: '0.5rem' }}>
FORUM UNAVAILABLE
</h1>
<p style={{ color: 'var(--color-text-muted)', fontSize: '0.8rem', marginTop: '1rem', fontFamily: 'var(--font-mono)' }}>
The forum has been temporarily disabled by an administrator.
</p>
</div>
);
}
return ( return (
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '4rem 1.5rem' }}> <div style={{ maxWidth: '960px', margin: '0 auto', padding: '4rem 1.5rem' }}>
{/* Header */} {/* Header */}