From 1d2dd17cd24c1a9fe44842d67497604364a5cfe2 Mon Sep 17 00:00:00 2001 From: Charlie Date: Fri, 27 Mar 2026 21:18:44 +1000 Subject: [PATCH 01/16] Wide-ranging mobile responsiveness improvements --- index.html | 5 + package.json | 1 + .../divisions/AirportPointEditor.jsx | 12 +- .../divisions/DivisionManagement.jsx | 6 +- src/components/home/Airports.jsx | 6 +- src/components/home/Documentation.jsx | 2 +- src/components/home/Hero.jsx | 38 +--- src/components/home/Support.jsx | 7 +- src/components/shared/Dropdown.jsx | 69 +++++--- src/components/shared/Tooltip.jsx | 7 +- src/components/staff/AirportManagement.jsx | 28 ++- src/components/staff/BanManagement.jsx | 142 ++++++++------- src/components/staff/ContactMessages.jsx | 93 ++++++---- .../staff/ContributionManagement.jsx | 4 +- src/components/staff/DivisionManagement.jsx | 78 ++++---- src/components/staff/FAQManagement.jsx | 166 ++++++++++++++---- src/components/staff/PackagesManagement.jsx | 72 ++++---- src/components/staff/ReleaseManagement.jsx | 148 +++++++--------- src/components/staff/UserManagement.jsx | 40 ++++- src/components/staff/VatSysProfiles.jsx | 2 +- src/components/staff/notamManagement.jsx | 60 ++++--- src/pages/Account.jsx | 50 +++--- src/pages/Changelog.jsx | 40 +++-- src/pages/ContributeDetails.jsx | 18 +- src/pages/ContributionDashboard.jsx | 54 +++--- src/pages/GlobalStatus.jsx | 6 +- src/pages/StaffDashboard.jsx | 82 ++++++++- 27 files changed, 705 insertions(+), 531 deletions(-) diff --git a/index.html b/index.html index 4e0baaf..8ee7fcd 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,11 @@ + diff --git a/package.json b/package.json index d389783..b4c8054 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint-plugin-react-refresh": "^0.4.26", "globals": "^16.5.0", "prettier": "^3.7.4", + "react-grab": "^0.1.29", "tailwindcss": "^4.1.18", "vite": "^7.3.0" } diff --git a/src/components/divisions/AirportPointEditor.jsx b/src/components/divisions/AirportPointEditor.jsx index a53af15..70addb3 100644 --- a/src/components/divisions/AirportPointEditor.jsx +++ b/src/components/divisions/AirportPointEditor.jsx @@ -1367,6 +1367,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' useEffect(() => { drawingCoordsRef.current = drawingCoords; }, [drawingCoords]); + useEffect(() => { if (uploadState.status === 'success') { const t = setTimeout(() => { @@ -2570,7 +2571,10 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' } return ( -
+

@@ -2612,8 +2616,8 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' ); })()}

-
-
+
+
{airportMetaLoading && !derivedCenter && (
@@ -2772,7 +2776,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' )}
-
+
{(selectedId || creatingNew) && (
{creatingNew && !selectedId && ( diff --git a/src/components/divisions/DivisionManagement.jsx b/src/components/divisions/DivisionManagement.jsx index 1195507..572bbcb 100644 --- a/src/components/divisions/DivisionManagement.jsx +++ b/src/components/divisions/DivisionManagement.jsx @@ -1077,7 +1077,7 @@ const DivisionManagement = () => { return ( -
+
{/* Header */}
@@ -1120,7 +1120,9 @@ const DivisionManagement = () => { members.map((member) => { const isSelf = String(currentUserId) === String(member.vatsim_id); const removeDisabled = - !canManageMembers || isSelf || (!canManageAsStaff && member.role === 'nav_head'); + !canManageMembers || + isSelf || + (!canManageAsStaff && member.role === 'nav_head'); return (
{
-
+

Global BARS Status

-
+
{
-
-
- {previewOptions.map((option) => { - const isSelected = option === selectedPreview; - return ( - - ); - })} -
- -
+
+
- {selectedPreview} Placeholder + Video Placeholder
diff --git a/src/components/home/Support.jsx b/src/components/home/Support.jsx index f1f74e0..7801f3f 100644 --- a/src/components/home/Support.jsx +++ b/src/components/home/Support.jsx @@ -4,9 +4,6 @@ import { Button } from '../shared/Button'; export const Support = () => { return (
- {/* Background decoration*/} -
-
{/* Header */}
@@ -21,8 +18,8 @@ export const Support = () => { {/* Documentation Card */}
-
- +
+

Documentation

diff --git a/src/components/shared/Dropdown.jsx b/src/components/shared/Dropdown.jsx index 62c4005..7cbdef4 100644 --- a/src/components/shared/Dropdown.jsx +++ b/src/components/shared/Dropdown.jsx @@ -24,7 +24,8 @@ export function Dropdown({ const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); - const currentOption = options.find((opt) => opt.value === value); + const currentOption = options.find((opt) => !opt.isHeader && opt.value === value); + const TriggerIcon = currentOption?.icon || null; // Close dropdown when clicking outside useEffect(() => { @@ -64,7 +65,8 @@ export function Dropdown({ disabled ? 'opacity-50 cursor-not-allowed' : '' }`} > - + + {TriggerIcon && } {currentOption?.label || placeholder} - {options.map((option, index) => ( - - ))} + {options.map((option, index) => { + if (option.isHeader) { + return ( +
+

+ {option.label} +

+
+ ); + } + const OptionIcon = option.icon; + return ( + + ); + })}
)}
@@ -106,8 +121,10 @@ export function Dropdown({ Dropdown.propTypes = { options: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.string.isRequired, + value: PropTypes.string, label: PropTypes.string.isRequired, + icon: PropTypes.elementType, + isHeader: PropTypes.bool, }) ), value: PropTypes.string, diff --git a/src/components/shared/Tooltip.jsx b/src/components/shared/Tooltip.jsx index fcc1aa7..a8b19b1 100644 --- a/src/components/shared/Tooltip.jsx +++ b/src/components/shared/Tooltip.jsx @@ -1,12 +1,14 @@ import PropTypes from 'prop-types'; -export const Tooltip = ({ children, content, className = '' }) => { +export const Tooltip = ({ children, content, className = '', open = false }) => { if (!content) return children; return (
{children} -
+
{content} {/* Arrow */}
@@ -19,4 +21,5 @@ Tooltip.propTypes = { children: PropTypes.node.isRequired, content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, className: PropTypes.string, + open: PropTypes.bool, }; diff --git a/src/components/staff/AirportManagement.jsx b/src/components/staff/AirportManagement.jsx index 416f474..9f3efcd 100644 --- a/src/components/staff/AirportManagement.jsx +++ b/src/components/staff/AirportManagement.jsx @@ -27,7 +27,7 @@ const AirportCard = ({ airport, onApprove, loadingState, onInfoClick }) => { return (
-

+

{airport.icao} @@ -116,7 +116,6 @@ AirportCard.propTypes = { const AirportManagement = () => { const [airports, setAirports] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [loadingState, setLoadingState] = useState({ id: null, action: null }); const [searchTerm, setSearchTerm] = useSearchQuery(); const [toast, setToast] = useState(null); @@ -151,7 +150,11 @@ const AirportManagement = () => { setAirports(sortedAirports); } catch (err) { - setError(err.message); + showToast({ + title: 'Failed to load airports', + description: err.message, + variant: 'destructive', + }); } finally { setLoading(false); } @@ -190,7 +193,6 @@ const AirportManagement = () => { variant: 'success', }); } catch (err) { - setError(err.message); showToast({ title: 'Error', description: err.message, @@ -274,18 +276,18 @@ const AirportManagement = () => {

Manage and review division airports

- + {pendingCount} pending -
+
@@ -298,10 +300,6 @@ const AirportManagement = () => {

Loading airports...

- ) : error ? ( -
- {error} -
) : !filteredAirports?.length ? (
@@ -325,10 +323,10 @@ const AirportManagement = () => { type="button" onClick={() => setCurrentPage((page) => Math.max(1, page - 1))} disabled={safePage === 1} - className="justify-self-start flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 hover:bg-zinc-700/60 disabled:opacity-50 disabled:cursor-not-allowed" + aria-label="Previous page" + className="justify-self-start flex items-center p-2 rounded-lg bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 hover:bg-zinc-700/60 disabled:opacity-50 disabled:cursor-not-allowed" > - Previous Page {safePage} of{' '} @@ -338,9 +336,9 @@ const AirportManagement = () => { type="button" onClick={() => setCurrentPage((page) => Math.min(totalPages, page + 1))} disabled={safePage === totalPages} - className="justify-self-end flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 hover:bg-zinc-700/60 disabled:opacity-50 disabled:cursor-not-allowed" + aria-label="Next page" + className="justify-self-end flex items-center p-2 rounded-lg bg-zinc-800 border border-zinc-700 text-sm text-zinc-300 hover:bg-zinc-700/60 disabled:opacity-50 disabled:cursor-not-allowed" > - Next
diff --git a/src/components/staff/BanManagement.jsx b/src/components/staff/BanManagement.jsx index 4ff7c31..0e5256f 100644 --- a/src/components/staff/BanManagement.jsx +++ b/src/components/staff/BanManagement.jsx @@ -2,17 +2,10 @@ import { useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { getVatsimToken } from '../../utils/cookieUtils'; import { formatLocalDateTime } from '../../utils/dateUtils'; +import { Card } from '../shared/Card'; import { Dialog } from '../shared/Dialog'; -import { - AlertTriangle, - AlertOctagon, - Ban as BanIcon, - Check, - Loader, - Trash2, - UserX, - FileText, -} from 'lucide-react'; +import { Toast } from '../shared/Toast'; +import { AlertOctagon, Ban as BanIcon, Loader, Trash2, UserX, FileText } from 'lucide-react'; const API_BASE = 'https://v2.stopbars.com'; @@ -21,8 +14,12 @@ export default function BanManagement() { const [searchParams, setSearchParams] = useSearchParams(); const [bans, setBans] = useState([]); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); + const [toast, setToast] = useState({ + show: false, + title: '', + description: '', + variant: 'default', + }); const [viewingReason, setViewingReason] = useState(null); // { targetId, reason } const [removingBan, setRemovingBan] = useState(null); // targetId to remove const [isRemovingBan, setIsRemovingBan] = useState(false); @@ -56,14 +53,18 @@ export default function BanManagement() { const fetchBans = async () => { setLoading(true); - setError(null); try { const res = await fetch(`${API_BASE}/bans`, { headers }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`); setBans(Array.isArray(data?.bans) ? data.bans : []); } catch (e) { - setError(e.message || 'Failed to load bans'); + setToast({ + show: true, + title: 'Failed to Load Bans', + description: e.message || 'Failed to load bans.', + variant: 'destructive', + }); } finally { setLoading(false); } @@ -71,25 +72,26 @@ export default function BanManagement() { useEffect(() => { if (!token) { - setError('Authentication required.'); + setToast({ + show: true, + title: 'Authentication Required', + description: 'A valid VATSIM token is required to manage bans.', + variant: 'destructive', + }); return; } fetchBans(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); - // Auto-dismiss success after ~4 seconds - useEffect(() => { - if (!success) return; - const t = setTimeout(() => setSuccess(null), 4000); - return () => clearTimeout(t); - }, [success]); - const handleCreateBan = async () => { - setError(null); - setSuccess(null); if (!vatsimId.trim()) { - setError('VATSIM CID is required'); + setToast({ + show: true, + title: 'Validation Error', + description: 'VATSIM CID is required.', + variant: 'destructive', + }); return; } const body = { @@ -106,13 +108,23 @@ export default function BanManagement() { }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`); - setSuccess('Ban created/updated successfully'); + setToast({ + show: true, + title: 'Ban Applied', + description: 'The ban has been successfully created or updated.', + variant: 'success', + }); setVatsimId(''); setReason(''); setExpiresAtLocal(''); await fetchBans(); } catch (e) { - setError(e.message || 'Failed to create ban'); + setToast({ + show: true, + title: 'Failed to Apply Ban', + description: e.message || 'Failed to create ban.', + variant: 'destructive', + }); } finally { setLoading(false); } @@ -120,8 +132,6 @@ export default function BanManagement() { const handleRemoveBan = async () => { if (!removingBan) return; - setError(null); - setSuccess(null); try { setIsRemovingBan(true); const res = await fetch(`${API_BASE}/bans/${encodeURIComponent(removingBan)}`, { @@ -132,11 +142,21 @@ export default function BanManagement() { const data = await res.json().catch(() => ({})); throw new Error(data?.error || `${res.status} ${res.statusText}`); } - setSuccess('Ban removed'); + setToast({ + show: true, + title: 'Ban Removed', + description: 'The ban has been removed', + variant: 'success', + }); setRemovingBan(null); await fetchBans(); } catch (e) { - setError(e.message || 'Failed to remove ban'); + setToast({ + show: true, + title: 'Failed to Remove Ban', + description: e.message || 'Failed to remove ban.', + variant: 'destructive', + }); } finally { setIsRemovingBan(false); } @@ -226,26 +246,8 @@ export default function BanManagement() { )}
- {/* Status Messages */} - {(error || success) && ( -
- {error ? ( - - ) : ( - - )} -

- {error || success} -

-
- )} - {/* Create / Update Ban */} -
+
@@ -253,7 +255,7 @@ export default function BanManagement() {

Create / Update Ban

-
+
@@ -262,11 +264,11 @@ export default function BanManagement() { value={vatsimId} onChange={(e) => setVatsimId(e.target.value.replace(/[^0-9]/g, ''))} placeholder="e.g., 1234567" - className="w-full px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" + className="w-full min-w-0 px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" inputMode="numeric" />
-
+
@@ -275,10 +277,10 @@ export default function BanManagement() { value={reason} onChange={(e) => setReason(e.target.value)} placeholder="Ban reason (optional)" - className="w-full px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" + className="w-full min-w-0 px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" />
-
+
@@ -286,32 +288,20 @@ export default function BanManagement() { type="datetime-local" value={expiresAtLocal} onChange={(e) => setExpiresAtLocal(e.target.value)} - className="w-full px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" + className="w-full min-w-0 px-4 py-2.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 transition-all" />
-
+
-
-
+ {/* Existing Bans */}
@@ -374,7 +364,7 @@ export default function BanManagement() { icon={AlertOctagon} iconColor="red" title="Remove Ban" - description={`This action will remove the ban and allow ${removingBan} to access and use all BARS services again.`} + description={`Removing this ban will restore full access to BARS services for VATSIM CID ${removingBan}. This action cannot be undone.`} isLoading={isRemovingBan} closeOnBackdrop={!isRemovingBan} closeOnEscape={!isRemovingBan} @@ -394,6 +384,14 @@ export default function BanManagement() { }, ]} > + + setToast((t) => ({ ...t, show: false }))} + />
); } diff --git a/src/components/staff/ContactMessages.jsx b/src/components/staff/ContactMessages.jsx index eccf4ae..85c7efb 100644 --- a/src/components/staff/ContactMessages.jsx +++ b/src/components/staff/ContactMessages.jsx @@ -2,12 +2,11 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { getVatsimToken } from '../../utils/cookieUtils'; import { Dialog } from '../shared/Dialog'; +import { Toast } from '../shared/Toast'; import { - AlertTriangle, Loader, Trash2, Mail, - CheckCircle2, MessageSquare, XCircle, AlertOctagon, @@ -56,17 +55,18 @@ export default function ContactMessages() { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); + const [toast, setToast] = useState({ + show: false, + title: '', + description: '', + variant: 'default', + }); const [selectedId, setSelectedId] = useState(null); const [updatingStatusId, setUpdatingStatusId] = useState(null); const [deletingMessage, setDeletingMessage] = useState(null); const [isDeletingMessage, setIsDeletingMessage] = useState(false); - const clearBanners = () => { - setError(null); - setSuccess(null); - }; + const clearBanners = () => {}; const fetchMessages = useCallback(async () => { if (!token) return; @@ -93,7 +93,12 @@ export default function ContactMessages() { }); setMessages(normalized); } catch (e) { - setError(e.message || 'Error fetching messages'); + setToast({ + show: true, + title: 'Failed to load messages', + description: e.message || 'Error fetching messages', + variant: 'destructive', + }); } finally { setLoading(false); } @@ -104,14 +109,7 @@ export default function ContactMessages() { }, [fetchMessages]); // Auto-dismiss success messages after 3 seconds - useEffect(() => { - if (success) { - const timer = setTimeout(() => { - setSuccess(null); - }, 3000); - return () => clearTimeout(timer); - } - }, [success]); + // (handled by Toast component) const filteredMessages = messages; // Direct list (already newest first) @@ -143,9 +141,19 @@ export default function ContactMessages() { setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...(updatedObj || {}), status: newStatus } : m)) ); - setSuccess('Status updated'); + setToast({ + show: true, + title: 'Status updated', + description: `Status changed to ${newStatus}.`, + variant: 'success', + }); } catch (e) { - setError(e.message || 'Failed to update status'); + setToast({ + show: true, + title: 'Failed to update status', + description: e.message || 'Failed to update status', + variant: 'destructive', + }); } finally { setUpdatingStatusId(null); } @@ -168,10 +176,20 @@ export default function ContactMessages() { // 204 No Content expected setMessages((prev) => prev.filter((m) => m.id !== id)); if (selectedId === id) setSelectedId(null); - setSuccess('Message deleted successfully'); + setToast({ + show: true, + title: 'Message deleted', + description: 'The message has been deleted successfully.', + variant: 'success', + }); setDeletingMessage(null); } catch (e) { - setError(e.message || 'Failed to delete message'); + setToast({ + show: true, + title: 'Failed to delete message', + description: e.message || 'Failed to delete message', + variant: 'destructive', + }); } finally { setIsDeletingMessage(false); } @@ -204,23 +222,11 @@ export default function ContactMessages() {
{/* Status Messages */} - {error && ( -
- -

{error}

-
- )} - {success && ( -
- -

{success}

-
- )} {/* Message Grid */}
{/* List */} -
+
{filteredMessages.length === 0 ? (
@@ -311,7 +317,12 @@ export default function ContactMessages() { if (selectedMessage.email) { navigator.clipboard.writeText(selectedMessage.email).catch(() => {}); window.open('https://mail.stopbars.com', '_blank', 'noopener'); - setSuccess('Email copied & ZoHo opened'); + setToast({ + show: true, + title: 'Email copied & opened', + description: '', + variant: 'success', + }); } }} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-zinc-800/50 border border-zinc-700/50 text-xs font-medium text-zinc-300 hover:bg-zinc-800 hover:border-zinc-600 transition-all" @@ -330,7 +341,7 @@ export default function ContactMessages() {
{/* Message Content */} -
+

{selectedMessage.message || selectedMessage.body || '(No message content)'}

@@ -346,7 +357,7 @@ export default function ContactMessages() { )}
) : ( -
+

Select a message to view details

@@ -399,6 +410,14 @@ export default function ContactMessages() {
+ + setToast((t) => ({ ...t, show: false }))} + />
); } diff --git a/src/components/staff/ContributionManagement.jsx b/src/components/staff/ContributionManagement.jsx index 213f445..14bdc01 100644 --- a/src/components/staff/ContributionManagement.jsx +++ b/src/components/staff/ContributionManagement.jsx @@ -427,7 +427,7 @@ const ReviewModal = ({ contribution, onClose, onApprove, onReject, onError }) =>

Contribution Details

{/* Row 1: Airport ICAO | Simulator */} -
+

Airport ICAO

{contribution.airportIcao}

@@ -453,7 +453,7 @@ const ReviewModal = ({ contribution, onClose, onApprove, onReject, onError }) =>
{/* Row 2: Package | Submitted By */} -
+

Package

diff --git a/src/components/staff/DivisionManagement.jsx b/src/components/staff/DivisionManagement.jsx index 12bf98e..b02c7f9 100644 --- a/src/components/staff/DivisionManagement.jsx +++ b/src/components/staff/DivisionManagement.jsx @@ -11,7 +11,6 @@ import { TowerControl, Edit, Trash2, - AlertTriangle, ChevronDown, ChevronUp, Loader, @@ -24,7 +23,6 @@ import { const DivisionManagement = () => { const [divisions, setDivisions] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); const [expandedDivisions, setExpandedDivisions] = useState({}); const [divisionMembers, setDivisionMembers] = useState({}); const [divisionAirports, setDivisionAirports] = useState({}); @@ -59,7 +57,12 @@ const DivisionManagement = () => { const divisionsData = await response.json(); setDivisions(divisionsData); } catch (err) { - setError(err.message); + setToast({ + show: true, + title: 'Failed to load divisions', + description: err.message, + variant: 'destructive', + }); } finally { setLoading(false); } @@ -146,7 +149,6 @@ const DivisionManagement = () => { const handleDeleteDivision = async (divisionId) => { setIsDeletingDivision(true); - setError(''); const divisionName = deletingDivision?.name; @@ -184,12 +186,10 @@ const DivisionManagement = () => { const cancelDelete = () => { setDeletingDivision(null); setDeleteConfirmation(''); - setError(''); }; const handleEditDivision = async (divisionId, newName) => { setIsEditingDivision(true); - setError(''); try { const response = await fetch(`https://v2.stopbars.com/divisions/${divisionId}`, { @@ -238,12 +238,10 @@ const DivisionManagement = () => { const cancelEdit = () => { setEditingDivision(null); setNewDivisionName(''); - setError(''); }; const handleCreateDivision = async () => { setIsCreatingDivision(true); - setError(''); try { const response = await fetch('https://v2.stopbars.com/divisions', { @@ -293,32 +291,41 @@ const DivisionManagement = () => { setShowCreateDialog(false); setCreateDivisionName(''); setCreateDivisionHeadCid(''); - setError(''); }; if (loading) { return (
-
-
-
+ {/* Header skeleton */} +
+
+
+
+
+
+
+
+
+ {/* Card skeletons */}
{[1, 2, 3].map((i) => (
-
-
-
+ {/* Mobile: name/date stacked, buttons below */} +
+
+
-
-
-
-
+
+
+
+
+
@@ -330,18 +337,6 @@ const DivisionManagement = () => { ); } - if (error) { - return ( -
-

Division Management

-
- -

{error}

-
-
- ); - } - return (
@@ -354,7 +349,10 @@ const DivisionManagement = () => { {divisions.length} division{divisions.length !== 1 ? 's' : ''} - @@ -363,12 +361,6 @@ const DivisionManagement = () => {
{/* Status Messages */} - {error && ( -
- -

{error}

-
- )} {divisions.length > 0 ? (
@@ -389,7 +381,7 @@ const DivisionManagement = () => { >
-
+

{division.name}

@@ -399,7 +391,7 @@ const DivisionManagement = () => { {formatDate(division.created_at)}

-
+
+ +
+ + )} +
+
+ + +
+
+ {/* Mobile: question title */} +

+ {faq.question} +

+ + {/* Desktop: original layout */} +
{faqs.length > 1 && (
@@ -628,6 +710,14 @@ const FAQManagement = () => {
+ + setToast((t) => ({ ...t, show: false }))} + />
); }; diff --git a/src/components/staff/PackagesManagement.jsx b/src/components/staff/PackagesManagement.jsx index 3ca817a..424e692 100644 --- a/src/components/staff/PackagesManagement.jsx +++ b/src/components/staff/PackagesManagement.jsx @@ -1,17 +1,9 @@ import { useState } from 'react'; import { Button } from '../shared/Button'; import { Card, CardHeader, CardTitle, CardContent } from '../shared/Card'; +import { Toast } from '../shared/Toast'; import { getVatsimToken } from '../../utils/cookieUtils'; -import { - Upload, - Package, - Check, - X, - AlertTriangle, - Info, - FileArchive, - RefreshCw, -} from 'lucide-react'; +import { Upload, Package, Check, X, Info, FileArchive, RefreshCw } from 'lucide-react'; /** * PackagesManagement @@ -60,15 +52,19 @@ const PackagesManagement = () => { const [selectedType, setSelectedType] = useState('models'); const [file, setFile] = useState(null); const [dragActive, setDragActive] = useState(false); - const [error, setError] = useState(''); + const [toast, setToast] = useState({ + show: false, + title: '', + description: '', + variant: 'default', + }); const [success, setSuccess] = useState(null); // {type,key,size,sha256,url,etag} const [uploading, setUploading] = useState(false); const [showMeta, setShowMeta] = useState(false); const reset = () => { setFile(null); - setError(''); - setUploading(false); + setToast((t) => ({ ...t, show: false })); }; const validate = (f) => { @@ -84,11 +80,10 @@ const PackagesManagement = () => { if (!f) return; const v = validate(f); if (v) { - setError(v); + setToast({ show: true, title: 'Invalid file', description: v, variant: 'destructive' }); return; } setFile(f); - setError(''); }; const onDrop = (e) => { @@ -99,27 +94,31 @@ const PackagesManagement = () => { if (!f) return; const v = validate(f); if (v) { - setError(v); + setToast({ show: true, title: 'Invalid file', description: v, variant: 'destructive' }); return; } setFile(f); - setError(''); }; const handleUpload = async () => { if (uploading) return; const v = validate(file); if (v) { - setError(v); + setToast({ show: true, title: 'Invalid file', description: v, variant: 'destructive' }); return; } - setError(''); + setToast((t) => ({ ...t, show: false })); setSuccess(null); try { setUploading(true); const token = getVatsimToken(); if (!token) { - setError('Missing auth token'); + setToast({ + show: true, + title: 'Not authenticated', + description: 'Missing auth token. Please log in again.', + variant: 'destructive', + }); setUploading(false); return; } @@ -143,12 +142,24 @@ const PackagesManagement = () => { } const data = await res.json(); setSuccess(data.package || null); + // Show toast notification + setToast({ + show: true, + title: 'Package uploaded', + description: 'The package has been uploaded successfully.', + variant: 'success', + }); // Auto-clear file after success to avoid accidental reupload setFile(null); setShowMeta(true); setTimeout(() => setSuccess(null), 15000); // fade success after 15s } catch (err) { - setError(err.message); + setToast({ + show: true, + title: 'Upload failed', + description: err.message, + variant: 'destructive', + }); } finally { setUploading(false); } @@ -189,7 +200,6 @@ const PackagesManagement = () => { key={pt.id} onClick={() => { setSelectedType(pt.id); - setError(''); }} className={`px-4 py-2 rounded-lg text-sm font-medium border transition-all ${active ? 'bg-blue-600 border-blue-500 text-white' : 'bg-zinc-800 border-zinc-700 text-zinc-300 hover:bg-zinc-700/70'}`} > @@ -291,16 +301,6 @@ const PackagesManagement = () => { />
- {error && ( -
- - {error} - -
- )} - {success && (
@@ -376,6 +376,14 @@ const PackagesManagement = () => {
+ + setToast((t) => ({ ...t, show: false }))} + />
); }; diff --git a/src/components/staff/ReleaseManagement.jsx b/src/components/staff/ReleaseManagement.jsx index 4ef2018..167f23a 100644 --- a/src/components/staff/ReleaseManagement.jsx +++ b/src/components/staff/ReleaseManagement.jsx @@ -1,13 +1,13 @@ import { useState, useEffect } from 'react'; import { Card } from '../shared/Card'; import { Dialog } from '../shared/Dialog'; +import { Toast } from '../shared/Toast'; import { getVatsimToken } from '../../utils/cookieUtils'; import { Upload, Image as ImageIcon, Check, AlertTriangle, - History, X, Plus, Edit, @@ -23,7 +23,7 @@ import { } from 'lucide-react'; import { marked } from 'marked'; // Configure marked to treat single line breaks as
and enable GitHub-flavored markdown. -marked.setOptions({ +marked.use({ breaks: true, // so a single newline becomes a line break gfm: true, }); @@ -56,14 +56,17 @@ const ReleaseManagement = () => { const [image, setImage] = useState(null); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(''); - const [uploadSuccess, setUploadSuccess] = useState(''); + const [toast, setToast] = useState({ + show: false, + title: '', + description: '', + variant: 'default', + }); // Changelog edit state const [editReleaseId, setEditReleaseId] = useState(''); const [newChangelog, setNewChangelog] = useState(''); const [updating, setUpdating] = useState(false); - const [updateError, setUpdateError] = useState(''); - const [updateSuccess, setUpdateSuccess] = useState(''); // Releases list state const [releases, setReleases] = useState([]); @@ -360,8 +363,6 @@ const ReleaseManagement = () => { const resetUpdateForm = () => { setEditReleaseId(''); setNewChangelog(''); - setUpdateError(''); - // Don't clear success message here - let it show for 4 seconds }; const renderMarkdown = (md) => { @@ -415,7 +416,6 @@ const ReleaseManagement = () => { const executeUpload = async () => { if (!pendingUploadData) return; setUploadError(''); - setUploadSuccess(''); setConfirmOpen(false); try { setUploading(true); @@ -442,7 +442,12 @@ const ReleaseManagement = () => { } throw new Error(message); } - setUploadSuccess('Release created successfully'); + setToast({ + show: true, + title: 'Release Published', + description: 'The new release has been published and is now available for download.', + variant: 'success', + }); resetUploadForm(); setIsAdding(false); fetchReleases(productFilter); // Refresh the releases list @@ -451,7 +456,6 @@ const ReleaseManagement = () => { } finally { setUploading(false); setPendingUploadData(null); - setTimeout(() => setUploadSuccess(''), 4000); } }; @@ -462,14 +466,12 @@ const ReleaseManagement = () => { setShowProductDropdown(false); setShowFilterDropdown(false); resetUploadForm(); - setUploadSuccess(''); // Clear any previous success message }; const handleStartUpdate = () => { setIsUpdating(true); setIsAdding(false); resetUpdateForm(); - setUpdateSuccess(''); // Clear any previous success message }; const handleCancel = () => { @@ -479,27 +481,38 @@ const ReleaseManagement = () => { setShowFilterDropdown(false); resetUploadForm(); resetUpdateForm(); - setUpdateSuccess(''); // Clear success message when canceling - setUploadSuccess(''); // Clear upload success message when canceling }; // submitUpload replaced by confirmation modal flow const submitChangelogUpdate = async (e) => { e.preventDefault(); - setUpdateError(''); - setUpdateSuccess(''); if (!editReleaseId.trim()) { - setUpdateError('Release ID is required'); + setToast({ + show: true, + title: 'Validation Error', + description: 'Release ID is required.', + variant: 'destructive', + }); return; } if (!newChangelog.trim()) { - setUpdateError('Changelog content is required'); + setToast({ + show: true, + title: 'Validation Error', + description: 'Changelog content is required.', + variant: 'destructive', + }); return; } if (newChangelog.length > 20000) { - setUpdateError('Changelog exceeds 20,000 character limit'); + setToast({ + show: true, + title: 'Validation Error', + description: 'Changelog exceeds 20,000 character limit.', + variant: 'destructive', + }); return; } @@ -526,16 +539,23 @@ const ReleaseManagement = () => { throw new Error(message); } - setUpdateSuccess('Changelog updated successfully'); + setToast({ + show: true, + title: 'Changelog Updated', + description: 'The release changelog has been updated.', + variant: 'success', + }); fetchReleases(productFilter); // Refresh the releases list resetUpdateForm(); } catch (err) { - setUpdateError(err.message); + setToast({ + show: true, + title: 'Update Failed', + description: err.message, + variant: 'destructive', + }); } finally { setUpdating(false); - setTimeout(() => { - setUpdateSuccess(''); - }, 4000); } }; @@ -617,7 +637,7 @@ const ReleaseManagement = () => { e.stopPropagation(); setIsOpen(!isOpen); }} - className="flex items-center justify-between w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:border-zinc-500 text-white transition-all duration-200 hover:border-zinc-600 hover:bg-zinc-750 text-sm min-w-[180px]" + className="flex items-center justify-between w-full px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:border-zinc-500 text-white transition-all duration-200 hover:border-zinc-600 hover:bg-zinc-750 text-sm min-w-45" > {currentOption?.label || 'All Products'} @@ -670,7 +690,7 @@ const ReleaseManagement = () => { {!isAdding && !isUpdating && (
- {/* Success Messages */} - {uploadSuccess && ( -
- -
-

Release Published Successfully

-

- The new release has been published and is now available for download. -

-
- -
- )} - - {updateSuccess && ( -
- -
-

Changelog Updated Successfully

-

- The release changelog has been updated. -

-
- -
- )} - {/* Add New Release Section */} {isAdding && (
-

- - Create New Release -

+

Create New Release

{ {product === 'Installer' ? 'Installer File' : 'Release File'} * -
+
SimConnect.NET (External) @@ -1076,13 +1056,6 @@ const ReleaseManagement = () => { )}
- - {updateError && ( -
- - {updateError} -
- )}
)} @@ -1090,11 +1063,8 @@ const ReleaseManagement = () => { {/* Existing Releases Section */} {!isAdding && !isUpdating && (
-
-
- -

Existing Releases

-
+
+

Existing Releases

{renderFilterDropdown( productFilter, @@ -1144,15 +1114,13 @@ const ReleaseManagement = () => { {rel.id} - - {rel.product} - + {rel.product} {rel.version} {rel.created_at ? new Date(rel.created_at).toLocaleDateString() : '-'} - + {rel.changelog ? ( rel.changelog.slice(0, 60) ) : ( @@ -1251,7 +1219,7 @@ const ReleaseManagement = () => { {product === 'Installer' ? 'Installer File:' : 'ZIP File:'} - + {file?.name}
@@ -1267,7 +1235,7 @@ const ReleaseManagement = () => { )}
Promo Image: - + {image ? image.name : '(none)'}
@@ -1315,6 +1283,14 @@ const ReleaseManagement = () => {
)} + + setToast((t) => ({ ...t, show: false }))} + />
); }; diff --git a/src/components/staff/UserManagement.jsx b/src/components/staff/UserManagement.jsx index 8f57e71..c762d31 100644 --- a/src/components/staff/UserManagement.jsx +++ b/src/components/staff/UserManagement.jsx @@ -34,6 +34,7 @@ const USERS_PER_PAGE = 6; const TruncatedName = ({ name }) => { const textRef = useRef(null); const [isTruncated, setIsTruncated] = useState(false); + const [tooltipOpen, setTooltipOpen] = useState(false); useEffect(() => { const checkTruncation = () => { @@ -46,14 +47,37 @@ const TruncatedName = ({ name }) => { return () => window.removeEventListener('resize', checkTruncation); }, [name]); + // Close tooltip when clicking elsewhere + useEffect(() => { + if (!tooltipOpen) return; + const handler = () => setTooltipOpen(false); + document.addEventListener('click', handler); + return () => document.removeEventListener('click', handler); + }, [tooltipOpen]); + const content = ( -

+

{ + e.stopPropagation(); + setTooltipOpen((o) => !o); + } + : undefined + } + > {name}

); if (isTruncated) { - return {content}; + return ( + + {content} + + ); } return content; @@ -293,7 +317,7 @@ const UserManagement = () => {
{!loading && ( - + {totalUsers} users @@ -305,7 +329,7 @@ const UserManagement = () => { placeholder="Search users..." value={searchTerm} onChange={handleSearch} - className="pl-9 pr-4 py-2 bg-zinc-800/50 border border-zinc-700/50 rounded-lg text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 w-64 transition-all" + className="pl-9 pr-4 py-2 bg-zinc-800/50 border border-zinc-700/50 rounded-lg text-sm placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500/40 w-full sm:w-64 transition-all" />
@@ -564,10 +588,10 @@ const UserManagement = () => { Page {currentPage} of{' '} @@ -576,9 +600,9 @@ const UserManagement = () => {
diff --git a/src/components/staff/VatSysProfiles.jsx b/src/components/staff/VatSysProfiles.jsx index 5524735..abe3ec0 100644 --- a/src/components/staff/VatSysProfiles.jsx +++ b/src/components/staff/VatSysProfiles.jsx @@ -345,7 +345,7 @@ const VatSysProfiles = () => {
-
+
ICAO
Name
diff --git a/src/components/staff/notamManagement.jsx b/src/components/staff/notamManagement.jsx index 3cfc8bc..ecf0069 100644 --- a/src/components/staff/notamManagement.jsx +++ b/src/components/staff/notamManagement.jsx @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react'; import { MessageSquareWarning, - AlertTriangle, Info, Loader, Plus, @@ -13,10 +12,10 @@ import { Tag, Send, HardDriveDownload, - CheckCircle2, Copy, Check, } from 'lucide-react'; +import { Toast } from '../shared/Toast'; import { getVatsimToken } from '../../utils/cookieUtils'; import DOMPurify from 'dompurify'; @@ -49,7 +48,12 @@ const NotamManagement = () => { const [showTypeDropdown, setShowTypeDropdown] = useState(false); const [showNewTypeDropdown, setShowNewTypeDropdown] = useState(false); const [saving, setSaving] = useState(false); - const [saveSuccess, setSaveSuccess] = useState(false); + const [toast, setToast] = useState({ + show: false, + title: '', + description: '', + variant: 'default', + }); const [hasEditChanges, setHasEditChanges] = useState(false); const [copied, setCopied] = useState(false); @@ -114,7 +118,7 @@ const NotamManagement = () => { bg: 'bg-amber-500/10', border: 'border-amber-500/20', text: 'text-amber-400', - icon: AlertTriangle, + icon: MessageSquareWarning, circle: 'bg-amber-400', }; case 'info': @@ -146,7 +150,7 @@ const NotamManagement = () => { bg: 'bg-red-500/10', border: 'border-red-500/20', text: 'text-red-400', - icon: AlertTriangle, + icon: MessageSquareWarning, circle: 'bg-red-400', }; default: @@ -275,10 +279,21 @@ const NotamManagement = () => { setEditType(type); // Show success message - setSaveSuccess(true); + setToast({ + show: true, + title: 'NOTAM Updated', + description: + 'The NOTAM has been published successfully. Changes may take a short time to propagate.', + variant: 'success', + }); } catch (err) { console.error('Error saving NOTAM:', err); - setError(err.message || 'Failed to save NOTAM'); + setToast({ + show: true, + title: 'NOTAM Failed', + description: err.message || 'Failed to save NOTAM', + variant: 'destructive', + }); } finally { setSaving(false); } @@ -417,26 +432,6 @@ const NotamManagement = () => {
- {/* Success Message */} - {saveSuccess && ( -
- -
-

NOTAM Updated Successfully

-

- The endpoint may take a short time to update. Users will see changes after cache - expires. -

-
- -
- )} - {/* Add New NOTAM Section */} {isAdding && (
@@ -557,7 +552,7 @@ const NotamManagement = () => { {/* Current NOTAM Display */} {!isEditing && !isAdding && (
-
+
@@ -574,7 +569,6 @@ const NotamManagement = () => {
) : error ? (
-

{error}

) : notamData?.notam ? ( @@ -605,6 +599,14 @@ const NotamManagement = () => { )}
)} + + setToast((t) => ({ ...t, show: false }))} + />
); }; diff --git a/src/pages/Account.jsx b/src/pages/Account.jsx index 4f71506..0a5948d 100644 --- a/src/pages/Account.jsx +++ b/src/pages/Account.jsx @@ -404,8 +404,8 @@ const Account = () => {

Account Settings

{staffRoles?.isStaff && ( - -
+ +
@@ -423,7 +423,7 @@ const Account = () => {
{/* Display Name Mode */} -
-
-
-

Preferred Display Name Mode

-

- Choose how your name appears publicly across BARS. -

-
+
+
+

Preferred Display Name Mode

+

+ Choose how your name appears publicly across BARS. +

{user?.display_name && ( -
-
+
+
Current: {user.display_name}
)}
-
+
{displayModeOptions.map((opt) => { const active = Number(displayMode) === opt.value; return ( @@ -631,8 +629,8 @@ const Account = () => { {userDivisions.length > 0 && ( - -
+ +

Your Divisions

@@ -646,7 +644,7 @@ const Account = () => { division && (

{division.name}

@@ -657,7 +655,7 @@ const Account = () => { onClick={() => (window.location.href = `/divisions/${division.id}/manage`) } - className="bg-blue-500 hover:bg-blue-600" + className="w-full sm:w-auto bg-blue-500 hover:bg-blue-600" > Manage Division @@ -670,32 +668,36 @@ const Account = () => { )} - -
+ +

Danger Zone

-
+

Sign Out

End your current session

-
-
+

Delete Account

Permanently delete your BARS account and all stored data.

- diff --git a/src/pages/Changelog.jsx b/src/pages/Changelog.jsx index 808c243..0a166d6 100644 --- a/src/pages/Changelog.jsx +++ b/src/pages/Changelog.jsx @@ -8,7 +8,7 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; // Configure marked to treat single line breaks as
and enable GitHub-flavored markdown. -marked.setOptions({ +marked.use({ breaks: true, // so a single newline becomes a line break gfm: true, }); @@ -282,14 +282,14 @@ const Changelog = () => { text-decoration: underline !important; } `} -
+
{/* Header Section */} -
-

Changelog

+
+

Changelog

{/* Filter Dropdown */} -
+
{ ) : (
{/* Main Content (Right Side) */} -
+
{filteredReleases.map((release, index) => (
0 ? 'mt-20' : ''}`} + className={`relative ${index > 0 ? 'mt-12 md:mt-20' : ''}`} > - {/* Timeline dot positioned relative to this release */} -
+ {/* Timeline dot — hidden on mobile, shown md+ */} +
{index === 0 && ( - <> -
-
- +
)} {/* Timeline line connecting to next release */} {index < filteredReleases.length - 1 && ( @@ -383,6 +380,11 @@ const Changelog = () => {
+ {/* Mobile date — shown below md */} +

+ {formatDate(release.created_at)} +

+ {/* Release content */}

{formatProductName(release.product)} v{release.version} diff --git a/src/pages/ContributeDetails.jsx b/src/pages/ContributeDetails.jsx index dd1baba..3aaec22 100644 --- a/src/pages/ContributeDetails.jsx +++ b/src/pages/ContributeDetails.jsx @@ -294,7 +294,7 @@ const ContributeDetails = () => {

{/* Skeleton for Top Packages */} -
+
{[...Array(4)].map((_, index) => (
{ {/* Top Packages */} {topPackages.length > 0 && ( -
+
{topPackages.map((pkg, index) => ( @@ -417,7 +417,7 @@ const ContributionDashboard = () => { disabled={!user} >
- + Your Contributions
@@ -434,7 +434,7 @@ const ContributionDashboard = () => { disabled={!user} >
- + Your Contributions
@@ -445,7 +445,7 @@ const ContributionDashboard = () => { {currentTab === 'user' && userContributionSummary && (

Your Contribution Summary

-
+
{userContributionSummary.total}
Total
@@ -502,7 +502,7 @@ const ContributionDashboard = () => { {filteredContributions.map((airport) => (
{/* Airport Header */} @@ -522,7 +522,7 @@ const ContributionDashboard = () => { .map((contribution) => (
{ : 'bg-zinc-800/50' }`} > -
-
- {contribution.scenery} +
+
+ + {contribution.scenery} + {contribution.simulator && ( { {contribution.status === 'approved' && ( )} {contribution.status === 'pending' && ( -
+
) : ( -
+
No reason provided
-
+
@@ -268,7 +268,7 @@ const GlobalStatus = () => { />
-
+
@@ -368,7 +368,7 @@ const GlobalStatus = () => { ) : (
-
+
diff --git a/src/pages/StaffDashboard.jsx b/src/pages/StaffDashboard.jsx index 4041652..68bbd80 100644 --- a/src/pages/StaffDashboard.jsx +++ b/src/pages/StaffDashboard.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { Layout } from '../components/layout/Layout'; import { Card } from '../components/shared/Card'; import { Button } from '../components/shared/Button'; @@ -21,6 +21,7 @@ import { FileUp, } from 'lucide-react'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { Dropdown } from '../components/shared/Dropdown'; import { getVatsimToken } from '../utils/cookieUtils'; // Import existing components @@ -280,6 +281,50 @@ const StaffDashboard = () => { }, 100); }; + // Mobile nav options — grouped with section headers and icons + const mobileNavOptions = useMemo(() => { + if (!staffRoles) return []; + const hasAccess = (tab) => tab.roles.some((r) => staffRoles[r.toLowerCase()] === 1); + const groups = [ + { + label: 'System Management', + ids: [ + 'userManagement', + 'staffManagement', + 'divisionManagement', + 'cacheManagement', + 'banManagement', + 'releaseManagement', + ], + }, + { + label: 'Content Management', + ids: [ + 'airportManagement', + 'contributionManagement', + 'notamManagement', + 'faqManagement', + 'contactMessages', + ], + }, + { + label: 'Data Management', + ids: ['packagesManagement', 'vatsysProfiles'], + }, + ]; + const opts = []; + for (const group of groups) { + const tabs = group.ids.map((id) => TABS[id]).filter((tab) => tab && hasAccess(tab)); + if (tabs.length > 0) { + opts.push({ label: group.label, isHeader: true }); + for (const tab of tabs) { + opts.push({ value: tab.id, label: tab.label, icon: tab.icon }); + } + } + } + return opts; + }, [staffRoles]); + // Check if user has access to a specific tab const hasTabAccess = (tab) => { if (!staffRoles) return false; @@ -311,7 +356,7 @@ const StaffDashboard = () => { return (
-
+
@@ -325,7 +370,7 @@ const StaffDashboard = () => { return (
-
+

{error}

@@ -342,8 +387,8 @@ const StaffDashboard = () => { return (
-
-
+
+

Staff Dashboard

@@ -363,7 +408,7 @@ const StaffDashboard = () => { })()}

-
+