Switched to typescript instead of javascript

This commit is contained in:
2026-03-11 00:17:15 +01:00 Verified
parent 4751f18ee6
commit 368dc41172
20 changed files with 550 additions and 263 deletions
@@ -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);
}}