feature : Connect front to backend #1
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { SettingsProvider, useSettings } from './contexts/SettingsContext';
|
||||
import { ProtectedRoute } from './components/shared/ProtectedRoute';
|
||||
import { PublicLayout } from './components/layout/PublicLayout';
|
||||
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 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 (
|
||||
<AuthProvider>
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route element={<PublicLayout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="studio" element={<StudioPage />} />
|
||||
<Route path="events" element={<EventsPage />} />
|
||||
<Route path="forum" element={<ForumPage />} />
|
||||
<Route path="forum/thread/:id" element={<ThreadPage />} />
|
||||
<Route path="bugs" element={<BugReportPage />} />
|
||||
<Route path="bugs/:id" element={<BugDetailPage />} />
|
||||
<Route path="forum" element={forumEnabled ? <ForumPage /> : <NotFoundPage />} />
|
||||
<Route path="forum/thread/:id" element={forumEnabled ? <ThreadPage /> : <NotFoundPage />} />
|
||||
<Route path="bugs" element={bugsEnabled ? <BugReportPage /> : <NotFoundPage />} />
|
||||
<Route path="bugs/:id" element={bugsEnabled ? <BugDetailPage /> : <NotFoundPage />} />
|
||||
<Route
|
||||
path="account"
|
||||
element={
|
||||
@@ -49,6 +52,17 @@ export default function App() {
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// ── App ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<SettingsProvider>
|
||||
<AppRoutes />
|
||||
</SettingsProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function BugDetailPage() {
|
||||
}, [id]);
|
||||
|
||||
const alreadyVoted = useMemo(
|
||||
() => !!user && !!bug && bug.meTooBugs.includes(user.id),
|
||||
() => !!user && !!bug && (bug.meTooBugs ?? []).includes(user.id),
|
||||
[user, bug]
|
||||
);
|
||||
const isOwnReport = useMemo(
|
||||
@@ -100,7 +100,7 @@ export default function BugDetailPage() {
|
||||
if (!user || !bug || alreadyVoted || isOwnReport) return;
|
||||
try {
|
||||
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 {
|
||||
// silently ignore
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export default function BugDetailPage() {
|
||||
return <Navigate to="/bugs" replace />;
|
||||
}
|
||||
|
||||
const metooCount = bug.meTooBugs.length;
|
||||
const metooCount = (bug.meTooBugs ?? []).length;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '860px', margin: '0 auto', padding: '3rem 1.5rem' }}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { bugsApi, settingsApi } from '../../utils/api';
|
||||
import { bugsApi } from '../../utils/api';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { timeAgo } from '../../utils/format';
|
||||
import type { BugReport, BugSeverity, BugStatus, BugReportFormData } from '../../types';
|
||||
@@ -78,7 +78,7 @@ function BugCard({ bug, highlight }: BugCardProps) {
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
▶ {bug.meTooBugs.length} {bug.meTooBugs.length === 1 ? 'user' : 'users'} have this
|
||||
▶ {(bug.meTooBugs ?? []).length} {(bug.meTooBugs ?? []).length === 1 ? 'user' : 'users'} have this
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -278,19 +278,14 @@ export default function BugReportPage() {
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const [bugs, setBugs] = useState<BugReport[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [bugsEnabled, setBugsEnabled] = useState(true);
|
||||
const [statusFilter, setStatusFilter] = useState<BugStatus | 'all'>('all');
|
||||
const [severityFilter, setSeverityFilter] = useState<BugSeverity | 'all'>('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
settingsApi.get().then((s) => setBugsEnabled(s.bugsEnabled)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const fetchBugs = useCallback(() => {
|
||||
setLoading(true);
|
||||
bugsApi.getBugs({ status: statusFilter, severity: severityFilter, limit: 100 })
|
||||
.then((res) => setBugs(res.data))
|
||||
.then((res) => setBugs(Array.isArray(res?.data) ? res.data : []))
|
||||
.catch(() => setBugs([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [statusFilter, severityFilter]);
|
||||
@@ -300,7 +295,7 @@ export default function BugReportPage() {
|
||||
const { myBugs, otherBugs } = useMemo(() => {
|
||||
const my: BugReport[] = [];
|
||||
const other: BugReport[] = [];
|
||||
bugs.forEach((b) => {
|
||||
(bugs ?? []).forEach((b) => {
|
||||
if (user && b.submittedById === user.id) my.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 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 (
|
||||
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '3rem 1.5rem' }}>
|
||||
{/* Header */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { forumApi, settingsApi } from '../../utils/api';
|
||||
import { forumApi } from '../../utils/api';
|
||||
import { timeAgo } from '../../utils/format';
|
||||
import type { ForumCategory, ForumThread } from '../../types';
|
||||
|
||||
@@ -132,20 +132,17 @@ export default function ForumPage() {
|
||||
const [threads, setThreads] = useState<ForumThread[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [forumEnabled, setForumEnabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
Promise.all([
|
||||
settingsApi.get(),
|
||||
forumApi.getCategories(),
|
||||
forumApi.getThreads({ limit: 200 }),
|
||||
])
|
||||
.then(([settings, cats, threadRes]) => {
|
||||
.then(([cats, threadRes]) => {
|
||||
if (cancelled) return;
|
||||
setForumEnabled(settings.forumEnabled);
|
||||
setCategories(cats);
|
||||
setThreads(threadRes.data);
|
||||
})
|
||||
@@ -169,20 +166,6 @@ export default function ForumPage() {
|
||||
);
|
||||
}, [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 (
|
||||
<div style={{ maxWidth: '960px', margin: '0 auto', padding: '4rem 1.5rem' }}>
|
||||
{/* Header */}
|
||||
|
||||
Reference in New Issue
Block a user