197 lines
9.7 KiB
React
197 lines
9.7 KiB
React
// frontend/src/pages/Dashboard.jsx
|
|
|
|
import { Calendar, ChevronDown, ChevronUp, History, Plus, SlidersHorizontal, Users, MapPin, Clock } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useOutletContext } from 'react-router-dom';
|
|
import TournamentForm from '../components/Forms/TournamentForm';
|
|
import Modal from '../components/UI/Modal';
|
|
import api, { WS_URL } from '../services/api';
|
|
|
|
// --- RESTORED DASHCARD DESIGN ---
|
|
const DashCard = ({ t, isAdmin, onSelect, onEdit }) => (
|
|
<div
|
|
onClick={() => onSelect(t.id)}
|
|
className="block bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-sm border border-zinc-200 dark:border-zinc-800 relative group hover:shadow-md hover:scale-[1.02] transition-all duration-200 will-change-transform transform-gpu cursor-pointer"
|
|
>
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div className="min-w-0 pr-4">
|
|
<h2 className="text-xl font-bold truncate text-zinc-900 dark:text-white group-hover:text-orange-500 dark:group-hover:text-orange-400 transition">
|
|
{t.name}
|
|
</h2>
|
|
<div className="text-xs text-zinc-400 dark:text-zinc-500 mt-1 font-mono flex items-center gap-2">
|
|
{/* Parse date safely */}
|
|
<span>{t.timestamp ? new Date(t.timestamp).toLocaleDateString() : 'TBD'}</span>
|
|
<span>{t.timestamp ? new Date(t.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''}</span>
|
|
</div>
|
|
</div>
|
|
<span className="bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 text-[10px] px-2 py-1 rounded font-mono border border-orange-200 dark:border-orange-800 uppercase tracking-tight shrink-0">
|
|
{t.type}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex gap-4 text-sm text-zinc-500 dark:text-zinc-400 items-center">
|
|
<div className="flex items-center gap-1.5 font-medium">
|
|
<Users size={16} className="text-orange-500" />
|
|
{t.team_count} Teams
|
|
</div>
|
|
<div className="flex items-center gap-1.5 font-medium">
|
|
<MapPin size={16} className="text-orange-500" />
|
|
{t.court_count} Courts
|
|
</div>
|
|
|
|
{isAdmin && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); onEdit(t); }}
|
|
className="ml-auto hover:text-orange-500 transition z-10 h-8 w-8 flex items-center justify-center rounded-full text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
title="Tournament Settings"
|
|
>
|
|
<SlidersHorizontal size={18} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
export default function Dashboard() {
|
|
const { setNavTitle, setNavSubtitle, isAdmin, showSettings, setShowSettings } = useOutletContext();
|
|
|
|
const [tournaments, setTournaments] = useState([]);
|
|
const [editTarget, setEditTarget] = useState(null);
|
|
const [showPast, setShowPast] = useState(false);
|
|
const [showAllFuture, setShowAllFuture] = useState(false);
|
|
const navigate = useNavigate();
|
|
|
|
const loadDashboard = async () => {
|
|
try {
|
|
const res = await api.get('/tournaments');
|
|
const list = Array.isArray(res) ? res : (res.items || []);
|
|
setTournaments(list);
|
|
} catch (e) { console.error(e); }
|
|
};
|
|
|
|
useEffect(() => {
|
|
setNavTitle('Dashboard');
|
|
setNavSubtitle('');
|
|
loadDashboard();
|
|
localStorage.removeItem('volley_view');
|
|
|
|
let ws;
|
|
const connect = () => {
|
|
try {
|
|
ws = new WebSocket(WS_URL);
|
|
ws.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === 'dashboard_update') loadDashboard();
|
|
};
|
|
} catch (err) { }
|
|
};
|
|
connect();
|
|
return () => { if (ws) ws.close(); };
|
|
}, []);
|
|
|
|
const handleEdit = (t) => {
|
|
setEditTarget(t);
|
|
setShowSettings(true);
|
|
};
|
|
|
|
const handleSuccess = () => {
|
|
setShowSettings(false);
|
|
setEditTarget(null);
|
|
loadDashboard();
|
|
};
|
|
|
|
const handleDelete = async (id) => {
|
|
if (window.confirm("Purge this tournament and all its history?")) {
|
|
await api.delete(`/tournaments/${id}`);
|
|
handleSuccess();
|
|
}
|
|
};
|
|
|
|
// Grouping Logic
|
|
const now = new Date();
|
|
const groups = { live: [], future: [], past: [] };
|
|
|
|
tournaments.forEach(t => {
|
|
const tDate = new Date(t.timestamp);
|
|
const isToday = tDate.toDateString() === now.toDateString();
|
|
if (tDate > now && !isToday) groups.future.push(t);
|
|
else if (isToday) groups.live.push(t);
|
|
else groups.past.push(t);
|
|
});
|
|
|
|
groups.future.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
groups.live.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
groups.past.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
|
|
const futureShow = showAllFuture ? groups.future : groups.future.slice(0, 4);
|
|
|
|
return (
|
|
<div className="h-full overflow-y-auto pt-8 sm:pt-12 pb-32">
|
|
<div className="container mx-auto max-w-5xl px-4 space-y-12">
|
|
|
|
{/* Create Button */}
|
|
<div className="flex justify-end">
|
|
{isAdmin && (
|
|
<button onClick={() => { setEditTarget(null); setShowSettings(true); }} className="bg-orange-600 hover:bg-orange-500 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 text-[10px] font-black uppercase tracking-wider shadow-xl shadow-orange-600/20 active:scale-95 transition">
|
|
<Plus size={16} strokeWidth={4} /> Create
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{groups.live.length > 0 && (
|
|
<section>
|
|
<h2 className="text-[10px] font-black text-green-500 uppercase tracking-[0.4em] mb-6 flex items-center gap-3">
|
|
<div className="w-2.5 h-2.5 bg-green-500 rounded-full animate-ping shadow-lg shadow-green-500/50" /> Live Events
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{groups.live.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<section>
|
|
<h2 className="text-[10px] font-black text-zinc-400 dark:text-zinc-600 uppercase tracking-[0.4em] mb-6 flex items-center gap-3">
|
|
<Calendar size={18} /> Upcoming
|
|
</h2>
|
|
{groups.future.length > 0 ? (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{futureShow.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
|
</div>
|
|
{groups.future.length > 4 && (
|
|
<div className="mt-8 text-center">
|
|
<button onClick={() => setShowAllFuture(!showAllFuture)} className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500 hover:text-orange-500 transition border-b-2 border-transparent hover:border-orange-500 pb-1 flex items-center justify-center gap-1 mx-auto">
|
|
{showAllFuture ? 'Show Less' : `Show All (${groups.future.length})`}
|
|
{showAllFuture ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : <div className="p-16 text-center rounded-3xl border-2 border-dashed border-zinc-300 dark:border-zinc-800 text-zinc-400 text-xs font-black uppercase tracking-[0.3em]">No Upcoming Events</div>}
|
|
</section>
|
|
|
|
{groups.past.length > 0 && (
|
|
<section>
|
|
<button onClick={() => setShowPast(!showPast)} className="w-full flex items-center justify-between group py-6 border-t border-zinc-300 dark:border-zinc-800 transition-colors hover:border-zinc-400">
|
|
<h2 className="text-[10px] font-black text-zinc-400 dark:text-zinc-600 uppercase tracking-[0.4em] mb-6 flex items-center gap-3"><History size={18} />Archive</h2>
|
|
{showPast ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
|
</button>
|
|
{showPast && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4 opacity-75 hover:opacity-100 transition-opacity">
|
|
{groups.past.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
</div>
|
|
|
|
<Modal isOpen={showSettings} onClose={() => { setShowSettings(false); setEditTarget(null); }} title={editTarget ? 'Modify Event' : 'Initialize Event'}>
|
|
<TournamentForm
|
|
tournamentId={editTarget?.id}
|
|
onSuccess={handleSuccess}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
} |