Compare commits
4 Commits
53740dc694
...
792816c6c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792816c6c8 | ||
|
|
a46dfde6d2 | ||
|
|
e8cd7e9562 | ||
|
|
2e42d67196 |
@@ -100,7 +100,7 @@ router.get('/', async (req: Request, res: Response): Promise<void> => {
|
|||||||
prisma.bugReport.count({ where }),
|
prisma.bugReport.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.json({ bugs: bugs.map(formatBug), total, page, pages: Math.ceil(total / limit) });
|
res.json({ data: bugs.map(formatBug), total, page, pages: Math.ceil(total / limit) });
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /api/bugs/:id
|
// GET /api/bugs/:id
|
||||||
|
|||||||
@@ -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,36 +20,49 @@ 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'));
|
||||||
|
|
||||||
|
// ── Routes (needs SettingsContext) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
const { forumEnabled, bugsEnabled, loaded } = useSettings();
|
||||||
|
|
||||||
|
if (!loaded) return <PageLoader />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<PublicLayout />}>
|
||||||
|
<Route index element={<HomePage />} />
|
||||||
|
<Route path="studio" element={<StudioPage />} />
|
||||||
|
<Route path="events" element={<EventsPage />} />
|
||||||
|
<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={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AccountPage />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="login" element={<LoginPage />} />
|
||||||
|
<Route path="register" element={<RegisterPage />} />
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── App ────────────────────────────────────────────────────────────────────────
|
// ── App ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Suspense fallback={<PageLoader />}>
|
<SettingsProvider>
|
||||||
<Routes>
|
<AppRoutes />
|
||||||
{/* Public Routes */}
|
</SettingsProvider>
|
||||||
<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="account"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<AccountPage />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route path="login" element={<LoginPage />} />
|
|
||||||
<Route path="register" element={<RegisterPage />} />
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Suspense>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useSettings } from '../../contexts/SettingsContext';
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
const { forumEnabled, bugsEnabled } = useSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
@@ -37,11 +39,11 @@ export function Footer() {
|
|||||||
<div className="section-label" style={{ marginBottom: '0.75rem' }}>Navigate</div>
|
<div className="section-label" style={{ marginBottom: '0.75rem' }}>Navigate</div>
|
||||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
||||||
{[
|
{[
|
||||||
{ to: '/', label: 'Home' },
|
{ to: '/', label: 'Home', show: true },
|
||||||
{ to: '/studio', label: 'Studio' },
|
{ to: '/studio', label: 'Studio', show: true },
|
||||||
{ to: '/forum', label: 'Forum' },
|
{ to: '/forum', label: 'Forum', show: forumEnabled },
|
||||||
{ to: '/bugs', label: 'Bug Reports' },
|
{ to: '/bugs', label: 'Bug Reports', show: bugsEnabled },
|
||||||
].map(({ to, label }) => (
|
].filter((item) => item.show).map(({ to, label }) => (
|
||||||
<li key={to}>
|
<li key={to}>
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
import { Link, NavLink, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { useSettings } from '../../contexts/SettingsContext';
|
||||||
|
|
||||||
const NAV_LINKS = [
|
const BASE_NAV_LINKS = [
|
||||||
{ to: '/', label: 'Home', end: true },
|
{ to: '/', label: 'Home', end: true, feature: null as null | 'forum' | 'bugs' },
|
||||||
{ to: '/studio', label: 'Studio', end: false },
|
{ to: '/studio', label: 'Studio', end: false, feature: null },
|
||||||
{ to: '/events', label: 'Events', end: false },
|
{ to: '/events', label: 'Events', end: false, feature: null },
|
||||||
{ to: '/forum', label: 'Forum', end: false },
|
{ to: '/forum', label: 'Forum', end: false, feature: 'forum' as const },
|
||||||
{ to: '/bugs', label: 'Bugs', end: false },
|
{ to: '/bugs', label: 'Bugs', end: false, feature: 'bugs' as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const { user, isAuthenticated, logout } = useAuth();
|
const { user, isAuthenticated, logout } = useAuth();
|
||||||
|
const { forumEnabled, bugsEnabled } = useSettings();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const navLinks = BASE_NAV_LINKS.filter(({ feature }) => {
|
||||||
|
if (feature === 'forum') return forumEnabled;
|
||||||
|
if (feature === 'bugs') return bugsEnabled;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const handleLogout = useCallback(() => {
|
const handleLogout = useCallback(() => {
|
||||||
logout();
|
logout();
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
@@ -85,7 +93,7 @@ export function Navbar() {
|
|||||||
|
|
||||||
{/* Desktop Nav */}
|
{/* Desktop Nav */}
|
||||||
<div className="hidden md:flex items-center gap-6">
|
<div className="hidden md:flex items-center gap-6">
|
||||||
{NAV_LINKS.map(({ to, label, end }) => (
|
{navLinks.map(({ to, label, end }) => (
|
||||||
<NavLink key={to} to={to} end={end} style={navLinkStyle}>
|
<NavLink key={to} to={to} end={end} style={navLinkStyle}>
|
||||||
{label}
|
{label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -148,7 +156,7 @@ export function Navbar() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.85rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.85rem' }}>
|
||||||
{NAV_LINKS.map(({ to, label, end }) => (
|
{navLinks.map(({ to, label, end }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
|
|||||||
31
nest-front/src/contexts/SettingsContext.tsx
Normal file
31
nest-front/src/contexts/SettingsContext.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { settingsApi } from '../utils/api';
|
||||||
|
|
||||||
|
interface SettingsContextValue {
|
||||||
|
forumEnabled: boolean;
|
||||||
|
bugsEnabled: boolean;
|
||||||
|
loaded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsContext = createContext<SettingsContextValue>({
|
||||||
|
forumEnabled: true,
|
||||||
|
bugsEnabled: true,
|
||||||
|
loaded: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function SettingsProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [value, setValue] = useState<SettingsContextValue>({ forumEnabled: true, bugsEnabled: true, loaded: false });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
settingsApi
|
||||||
|
.get()
|
||||||
|
.then((s) => setValue({ forumEnabled: s.forumEnabled, bugsEnabled: s.bugsEnabled, loaded: true }))
|
||||||
|
.catch(() => setValue({ forumEnabled: true, bugsEnabled: true, loaded: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettings(): SettingsContextValue {
|
||||||
|
return useContext(SettingsContext);
|
||||||
|
}
|
||||||
@@ -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