feature : Connect front to backend #1
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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' }}>
|
||||||
|
|||||||
@@ -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',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
▶ {bug.meTooBugs.length} {bug.meTooBugs.length === 1 ? 'user' : 'users'} have this
|
▶ {(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 */}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user