Switched to typescript instead of javascript
This commit is contained in:
@@ -1,29 +1,45 @@
|
||||
// frontend/src/pages/Dashboard.jsx
|
||||
// frontend/src/pages/Dashboard.tsx
|
||||
|
||||
import { Calendar, ChevronDown, ChevronUp, History, Plus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Calendar, ChevronDown, ChevronUp, History, Plus, PlusCircle, SlidersHorizontal } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import DashCard from '../components/Dashboard/DashCard';
|
||||
import TournamentForm from '../components/Forms/TournamentForm';
|
||||
import Modal from '../components/UI/Modal';
|
||||
import api, { WS_URL } from '../services/api';
|
||||
import { SlidersHorizontal, PlusCircle } from 'lucide-react';
|
||||
|
||||
|
||||
export interface TournamentType {
|
||||
id: string | number;
|
||||
timestamp: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface OutletContextType {
|
||||
setNavTitle: (title: string) => void;
|
||||
setNavSubtitle: (subtitle: string) => void;
|
||||
isAdmin: boolean;
|
||||
showSettings: boolean;
|
||||
setShowSettings: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { setNavTitle, setNavSubtitle, isAdmin, showSettings, setShowSettings } = useOutletContext();
|
||||
const { setNavTitle, setNavSubtitle, isAdmin, showSettings, setShowSettings } = useOutletContext<OutletContextType>();
|
||||
|
||||
const [tournaments, setTournaments] = useState([]);
|
||||
const [editTarget, setEditTarget] = useState(null);
|
||||
const [showPast, setShowPast] = useState(false);
|
||||
const [showAllFuture, setShowAllFuture] = useState(false);
|
||||
const [tournaments, setTournaments] = useState<TournamentType[]>([]);
|
||||
const [editTarget, setEditTarget] = useState<TournamentType | null>(null);
|
||||
const [showPast, setShowPast] = useState<boolean>(false);
|
||||
const [showAllFuture, setShowAllFuture] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const loadDashboard = async () => {
|
||||
try {
|
||||
const res = await api.get('/tournaments');
|
||||
const res: any = await api.get('/tournaments');
|
||||
const list = Array.isArray(res) ? res : (res.items || []);
|
||||
setTournaments(list);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,21 +48,24 @@ export default function Dashboard() {
|
||||
loadDashboard();
|
||||
localStorage.removeItem('volley_view');
|
||||
|
||||
let ws;
|
||||
let ws: WebSocket;
|
||||
const connect = () => {
|
||||
try {
|
||||
ws = new WebSocket(WS_URL);
|
||||
ws.onmessage = (e) => {
|
||||
ws.onmessage = (e: MessageEvent) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.type === 'dashboard_update') loadDashboard();
|
||||
};
|
||||
} catch (err) { }
|
||||
} catch (err) {
|
||||
console.error("WebSocket connection failed", err);
|
||||
}
|
||||
};
|
||||
connect();
|
||||
return () => { if (ws) ws.close(); };
|
||||
}, []);
|
||||
|
||||
const handleEdit = (t) => {
|
||||
return () => { if (ws) ws.close(); };
|
||||
}, [setNavTitle, setNavSubtitle]);
|
||||
|
||||
const handleEdit = (t: TournamentType) => {
|
||||
setEditTarget(t);
|
||||
setShowSettings(true);
|
||||
};
|
||||
@@ -57,28 +76,32 @@ export default function Dashboard() {
|
||||
loadDashboard();
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
const handleDelete = async (id: string | number) => {
|
||||
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: [] };
|
||||
const groups: { live: TournamentType[]; future: TournamentType[]; past: TournamentType[] } = {
|
||||
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));
|
||||
groups.future.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
groups.live.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
groups.past.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
const futureShow = showAllFuture ? groups.future : groups.future.slice(0, 4);
|
||||
|
||||
@@ -86,7 +109,6 @@ export default function Dashboard() {
|
||||
<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 */}
|
||||
{isAdmin && (
|
||||
<div className="flex justify-center md:justify-end">
|
||||
<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">
|
||||
@@ -98,13 +120,13 @@ export default function Dashboard() {
|
||||
{groups.live.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-xs font-black text-green-500 uppercase tracking-[0.2em] mb-6 flex items-center gap-3">
|
||||
<span class="relative flex size-3">
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex size-3 rounded-full bg-green-500"></span>
|
||||
<span className="relative flex size-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||
<span className="relative inline-flex size-3 rounded-full bg-green-500"></span>
|
||||
</span> 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} />)}
|
||||
{groups.live.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -116,11 +138,11 @@ export default function Dashboard() {
|
||||
{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} />)}
|
||||
{futureShow.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
||||
</div>
|
||||
{groups.future.length > 4 && (
|
||||
<div className="mt-8 text-center">
|
||||
<button onClick={() => setShowAllFuture(!showAllFuture)} className="text-xs font-black uppercase tracking-[0.1em] 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">
|
||||
<button onClick={() => setShowAllFuture(!showAllFuture)} className="text-xs font-black uppercase tracking-widest 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>
|
||||
@@ -138,7 +160,7 @@ export default function Dashboard() {
|
||||
</button>
|
||||
{showPast && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 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} />)}
|
||||
{groups.past.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -1,23 +1,29 @@
|
||||
// frontend/src/pages/Login.jsx
|
||||
// frontend/src/pages/Login.tsx
|
||||
|
||||
import { Loader2, Volleyball } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import api from '../services/api';
|
||||
|
||||
interface TokenResponse {
|
||||
access_token: string;
|
||||
token_type?: string;
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const formData = new FormData(e.target);
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const res = await api.postForm('/auth/token', formData);
|
||||
const res = await api.postForm<TokenResponse>('/auth/token', formData);
|
||||
localStorage.setItem('volleyToken', res.access_token);
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
@@ -1,7 +1,7 @@
|
||||
// frontend/src/pages/Tournament.jsx
|
||||
// frontend/src/pages/Tournament.tsx
|
||||
|
||||
import { CalendarDays, Loader2, Network, SlidersHorizontal } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useOutletContext, useParams } from 'react-router-dom';
|
||||
import BracketView from '../components/Bracket/BracketView';
|
||||
import TournamentForm from '../components/Forms/TournamentForm';
|
||||
@@ -10,23 +10,67 @@ import ScoreModal from '../components/Tournament/ScoreModal';
|
||||
import Modal from '../components/UI/Modal';
|
||||
import api, { WS_URL } from '../services/api';
|
||||
|
||||
// --- Type Definitions ---
|
||||
interface Team { id: string | number; name: string; }
|
||||
interface Court { id: string | number; name: string; }
|
||||
|
||||
interface RawMatch {
|
||||
id: string | number;
|
||||
bracket_type: string;
|
||||
round_number: number;
|
||||
match_number: number;
|
||||
winner_next_match_id?: string | number | null;
|
||||
loser_next_match_id?: string | number | null;
|
||||
p1_team_id?: string | number | null;
|
||||
p2_team_id?: string | number | null;
|
||||
winner_team_id?: string | number | null;
|
||||
status: string;
|
||||
court_id: string | number;
|
||||
start_time?: string;
|
||||
sets?: { p1: number; p2: number }[];
|
||||
}
|
||||
|
||||
export interface ProcessedMatch extends RawMatch {
|
||||
bracket: string;
|
||||
round: number;
|
||||
number: number;
|
||||
p1: string;
|
||||
p2: string;
|
||||
winnerName: string | null;
|
||||
p1_is_real: boolean;
|
||||
p2_is_real: boolean;
|
||||
isReady: boolean;
|
||||
court: string;
|
||||
time: string;
|
||||
p1_sets: number;
|
||||
p2_sets: number;
|
||||
hasTeams: boolean;
|
||||
isFinished: boolean;
|
||||
}
|
||||
|
||||
interface OutletContextType {
|
||||
setNavTitle: (title: string) => void;
|
||||
setNavSubtitle: (subtitle: string) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
export default function Tournament() {
|
||||
const { id } = useParams();
|
||||
const { setNavTitle, setNavSubtitle, isAdmin } = useOutletContext();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { setNavTitle, setNavSubtitle, isAdmin } = useOutletContext<OutletContextType>();
|
||||
|
||||
const [matches, setMatches] = useState([]);
|
||||
const [view, setView] = useState(() => localStorage.getItem('volley_view') || 'bracket');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [scoreMatch, setScoreMatch] = useState(null);
|
||||
const wsRef = useRef(null);
|
||||
const [matches, setMatches] = useState<ProcessedMatch[]>([]);
|
||||
const [view, setView] = useState<string>(() => localStorage.getItem('volley_view') || 'bracket');
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||
const [scoreMatch, setScoreMatch] = useState<ProcessedMatch | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const handleViewChange = (newView) => {
|
||||
const handleViewChange = (newView: string) => {
|
||||
setView(newView);
|
||||
localStorage.setItem('volley_view', newView);
|
||||
};
|
||||
|
||||
const processMatches = (rawMatches, courts, teams) => {
|
||||
const processMatches = (rawMatches: RawMatch[], courts: Court[], teams: Team[]): ProcessedMatch[] => {
|
||||
if (!rawMatches) return [];
|
||||
|
||||
const gf1 = rawMatches.find(m =>
|
||||
@@ -35,15 +79,16 @@ export default function Tournament() {
|
||||
m.winner_next_match_id === m.loser_next_match_id
|
||||
);
|
||||
|
||||
let skippedResetMatchId = null;
|
||||
let skippedResetMatchId: string | number | null = null;
|
||||
if (gf1 && gf1.status === 'Finished' && gf1.winner_team_id === gf1.p1_team_id) {
|
||||
skippedResetMatchId = gf1.winner_next_match_id;
|
||||
skippedResetMatchId = gf1.winner_next_match_id || null;
|
||||
}
|
||||
|
||||
const courtMap = Object.fromEntries(courts.map(c => [c.id, c.name]));
|
||||
const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
|
||||
const courtMap: Record<string | number, string> = Object.fromEntries(courts.map(c => [c.id, c.name]));
|
||||
const teamMap: Record<string | number, string> = Object.fromEntries(teams.map(t => [t.id, t.name]));
|
||||
|
||||
const incoming: Record<string | number, { label: string; id: string | number }[]> = {};
|
||||
|
||||
const incoming = {};
|
||||
rawMatches.forEach(m => {
|
||||
const num = m.match_number;
|
||||
if (m.winner_next_match_id) {
|
||||
@@ -91,30 +136,40 @@ export default function Tournament() {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.get(`/tournaments/${id}`);
|
||||
const res: any = await api.get(`/tournaments/${id}`);
|
||||
setNavTitle(res.name);
|
||||
setNavSubtitle(new Date(res.timestamp).toLocaleDateString());
|
||||
setMatches(processMatches(res.matches, res.courts, res.teams));
|
||||
} catch (err) { console.error(err); } finally { setLoading(false); }
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
if (wsRef.current) return;
|
||||
|
||||
const connect = () => {
|
||||
const ws = new WebSocket(WS_URL);
|
||||
wsRef.current = ws;
|
||||
ws.onmessage = (e) => {
|
||||
ws.onmessage = (e: MessageEvent) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.type === 'tournament_update' && msg.id === id) fetchData();
|
||||
};
|
||||
ws.onclose = () => { wsRef.current = null; };
|
||||
};
|
||||
connect();
|
||||
return () => { if (wsRef.current?.readyState === 1) wsRef.current.close(); wsRef.current = null; };
|
||||
}, [id]);
|
||||
|
||||
const handleDeleteTournament = async (tId) => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
if (wsRef.current?.readyState === 1) wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
};
|
||||
}, [id, setNavTitle, setNavSubtitle]);
|
||||
|
||||
const handleDeleteTournament = async (tId: string | number) => {
|
||||
if (window.confirm("Purge this tournament?")) {
|
||||
await api.delete(`/tournaments/${tId}`);
|
||||
window.location.href = '/';
|
||||
@@ -150,12 +205,13 @@ export default function Tournament() {
|
||||
onClose={() => setScoreMatch(null)}
|
||||
match={scoreMatch}
|
||||
isAdmin={isAdmin}
|
||||
onClear={async (mid, c) => {
|
||||
onClear={async (mid: string | number, c: string) => {
|
||||
await api.delete(`/tournaments/${id}/matches/${mid}/score?code=${encodeURIComponent(c || '')}`);
|
||||
setScoreMatch(null);
|
||||
}}
|
||||
onSubmit={async (mid, s, c) => {
|
||||
onSubmit={async (mid: string | number, s: any, c: string) => {
|
||||
const method = scoreMatch.isFinished ? 'patch' : 'post';
|
||||
// @ts-ignore - Indexing api dynamically
|
||||
await api[method](`/tournaments/${id}/matches/${mid}/score`, { sets: s, code: c });
|
||||
setScoreMatch(null);
|
||||
}}
|
||||
Reference in New Issue
Block a user