Typscript final

This commit is contained in:
2026-03-11 16:44:09 +01:00 Verified
parent 42b28350de
commit edd9b064ec
24 changed files with 486 additions and 419 deletions
+59 -2
View File
@@ -1,4 +1,4 @@
# React + Vite # React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
@@ -13,4 +13,61 @@ The React Compiler is not enabled on this template because of its impact on dev
## Expanding the ESLint configuration ## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
+23
View File
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
-35
View File
@@ -1,35 +0,0 @@
// frontend/eslint.config.ts
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
...tseslint.configs.recommended,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
}
);
-3
View File
@@ -1,3 +0,0 @@
// frontend/global.d.ts
declare module '*.css';
+269 -262
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -5,8 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"build": "vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "tsc -b && eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest" "test": "vitest"
}, },
@@ -1,7 +1,7 @@
// frontend/src/components/Bracket/BracketView.tsx // frontend/src/components/Bracket/BracketView.tsx
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { MatchData } from '../../types'; import { type MatchData } from '../../types';
import Podium from "../Tournament/Podium"; import Podium from "../Tournament/Podium";
import MatchCard from "./MatchCard"; import MatchCard from "./MatchCard";
@@ -80,7 +80,7 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
const roundKeys = Object.keys(rounds).map(Number).sort((a, b) => a - b); const roundKeys = Object.keys(rounds).map(Number).sort((a, b) => a - b);
return roundKeys.map((r) => { return roundKeys.map((r) => {
let matchesInRound = rounds[r]; const matchesInRound = rounds[r];
matchesInRound.sort((a, b) => a.number - b.number); matchesInRound.sort((a, b) => a.number - b.number);
return ( return (
<div key={r} className="flex flex-col gap-10 z-10 w-64 shrink-0 justify-around"> <div key={r} className="flex flex-col gap-10 z-10 w-64 shrink-0 justify-around">
@@ -95,8 +95,8 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
const lb = matches.filter(m => m.bracket === 'Loser'); const lb = matches.filter(m => m.bracket === 'Loser');
const finals = matches.filter(m => m.bracket === 'Finals'); const finals = matches.filter(m => m.bracket === 'Finals');
let displayWb = [...wb]; const displayWb = [...wb];
let displayFinals = [...finals]; const displayFinals = [...finals];
if (!isDoubleElim && displayWb.length > 0) { if (!isDoubleElim && displayWb.length > 0) {
const maxRound = Math.max(...displayWb.map(m => m.round)); const maxRound = Math.max(...displayWb.map(m => m.round));
@@ -146,7 +146,6 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
)} )}
</div> </div>
<div className="flex flex-col justify-center gap-6 z-10"> <div className="flex flex-col justify-center gap-6 z-10">
{/* @ts-ignore - Assuming Podium was typed earlier or needs its own MatchData import */}
<Podium matches={matches} /> <Podium matches={matches} />
</div> </div>
</div> </div>
@@ -1,7 +1,7 @@
// frontend/src/components/Bracket/MatchCard.tsx // frontend/src/components/Bracket/MatchCard.tsx
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { MatchData } from '../../types'; import { type MatchData } from '../../types';
import { printName, stringToColor } from '../../utils/helpers'; import { printName, stringToColor } from '../../utils/helpers';
interface MatchCardProps { interface MatchCardProps {
@@ -48,7 +48,7 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
{ n: match.p1, s: match.p1_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p1_team_id, real: match.p1_is_real }, { n: match.p1, s: match.p1_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p1_team_id, real: match.p1_is_real },
{ n: match.p2, s: match.p2_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p2_team_id, real: match.p2_is_real } { n: match.p2, s: match.p2_sets, win: match.winner_team_id !== null && match.winner_team_id === match.p2_team_id, real: match.p2_is_real }
].map((p, i) => ( ].map((p, i) => (
<div key={i} className={`flex justify-between items-center ${p.win ? 'text-zinc-800 dark:text-zinc-50 print:text-black! font-black' : p.real ? 'text-zinc-500 dark:text-zinc-400 print:text-black!' : 'text-zinc-400 print:text-zinc-600! italic'}`}> <div key={i} className={`flex justify-between items-center ${p.win ? 'text-zinc-800 dark:text-zinc-50 print:text-black! font-black' : p.real ? 'text-zinc-600 dark:text-zinc-400 print:text-black!' : 'text-zinc-400 dark:text-zinc-600 print:text-zinc-600! italic'}`}>
<span className={`truncate text-xs tracking-tight pr-2 print:hidden ${p.win ? ' font-black text-orange-500' : 'font-bold'}`}>{p.n}</span> <span className={`truncate text-xs tracking-tight pr-2 print:hidden ${p.win ? ' font-black text-orange-500' : 'font-bold'}`}>{p.n}</span>
@@ -11,8 +11,8 @@ interface TournamentSettings {
type: string; type: string;
timestamp: string; timestamp: string;
duration: number; duration: number;
courts: any[]; courts: { name?: string }[];
teams: any[]; teams: { name?: string }[];
} }
interface TournamentFormProps { interface TournamentFormProps {
@@ -106,12 +106,13 @@ export default function TournamentForm({ tournamentId, onSuccess, onDelete }: To
await api.post('/tournaments', fullPayload); await api.post('/tournaments', fullPayload);
} }
onSuccess(); onSuccess();
} catch (err: any) { } catch (err: unknown) {
console.error(err); console.error(err);
if (Array.isArray(err.detail)) { const error = err as { detail?: string | Array<{ loc: string[]; msg: string }> };
setError(err.detail.map((e: any) => `${e.loc.join('.')}: ${e.msg}`).join(', ')); if (Array.isArray(error.detail)) {
setError(error.detail.map((e) => `${e.loc.join('.')}: ${e.msg}`).join(', '));
} else { } else {
setError(typeof err.detail === 'string' ? err.detail : "Error saving tournament"); setError(typeof error.detail === 'string' ? error.detail : "Error saving tournament");
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
+1 -1
View File
@@ -20,7 +20,7 @@ export interface OutletContextType {
} }
export default function Layout({ darkMode, setDarkMode }: LayoutProps) { export default function Layout({ darkMode, setDarkMode }: LayoutProps) {
const [isAdmin, setIsAdmin] = useState<boolean>(!!getToken()); const [isAdmin] = useState<boolean>(!!getToken());
const [navTitle, setNavTitle] = useState<string>(''); const [navTitle, setNavTitle] = useState<string>('');
const [navSubtitle, setNavSubtitle] = useState<string>(''); const [navSubtitle, setNavSubtitle] = useState<string>('');
const [showSettings, setShowSettings] = useState<boolean>(false); const [showSettings, setShowSettings] = useState<boolean>(false);
@@ -2,7 +2,7 @@
import { CheckCircle, Pencil, Plus, Trophy } from 'lucide-react'; import { CheckCircle, Pencil, Plus, Trophy } from 'lucide-react';
import React from 'react'; import React from 'react';
import { MatchData } from '../../types'; import { type MatchData } from '../../types';
import { printName, stringToColor } from '../../utils/helpers'; import { printName, stringToColor } from '../../utils/helpers';
interface ScheduleRowProps { interface ScheduleRowProps {
@@ -2,7 +2,7 @@
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { MatchData } from '../../types'; import { type MatchData } from '../../types';
import ScheduleRow from './ScheduleRow'; import ScheduleRow from './ScheduleRow';
interface ScheduleViewProps { interface ScheduleViewProps {
@@ -1,13 +1,10 @@
// frontend/src/components/Tournament/ScoreModal.tsx // frontend/src/components/Tournament/ScoreModal.tsx
import { type SetData } from '../../types';
import { Clock, Eraser, MapPin, Trophy } from 'lucide-react'; import { Clock, Eraser, MapPin, Trophy } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import Modal from '../UI/Modal'; import Modal from '../UI/Modal';
interface SetData {
p1: number | string;
p2: number | string;
}
interface MatchData { interface MatchData {
id: string | number; id: string | number;
@@ -37,8 +34,9 @@ const ScoreForm = ({ match, isAdmin, onSubmit, onClear }: ScoreFormProps) => {
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
await onSubmit(match.id, sets, code); await onSubmit(match.id, sets, code);
} catch (err: any) { } catch (err: unknown) {
setError(typeof err?.detail === 'string' ? err.detail : "Check code or scores"); const error = err as { detail?: string };
setError(typeof error?.detail === 'string' ? error.detail : "Check code or scores");
} }
}; };
+2 -2
View File
@@ -1,6 +1,6 @@
// frontend/src/components/UI/Modal.tsx // frontend/src/components/UI/Modal.tsx
import { LucideIcon, X } from 'lucide-react'; import { type LucideIcon, X } from 'lucide-react';
import React from 'react'; import React from 'react';
interface ModalProps { interface ModalProps {
@@ -19,7 +19,7 @@ export default function Modal({ isOpen, onClose, title, icon: Icon, children }:
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-black text-zinc-900 dark:text-white flex items-start gap-2"> <h2 className="text-xl font-black text-zinc-900 dark:text-white flex items-start gap-2">
{/* @ts-ignore - 'weight' is a specific prop if using Phosphor icons, but Lucide doesn't natively use it. Kept for compatibility. */} {/* @ts-expect-error - 'weight' is a specific prop if using Phosphor icons, but Lucide doesn't natively use it. */}
{Icon && <Icon weight="duotone" className="text-orange-500 mt-1 shrink-0" size={24} />} {Icon && <Icon weight="duotone" className="text-orange-500 mt-1 shrink-0" size={24} />}
<span>{title}</span> <span>{title}</span>
</h2> </h2>
+17 -12
View File
@@ -1,7 +1,7 @@
// frontend/src/pages/Dashboard.tsx // frontend/src/pages/Dashboard.tsx
import { Calendar, ChevronDown, ChevronUp, History, Plus, PlusCircle, SlidersHorizontal } from 'lucide-react'; import { Calendar, ChevronDown, ChevronUp, History, Plus, PlusCircle, SlidersHorizontal } from 'lucide-react';
import React, { useEffect, useState } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom'; import { useNavigate, useOutletContext } from 'react-router-dom';
import DashCard from '../components/Dashboard/DashCard'; import DashCard from '../components/Dashboard/DashCard';
import TournamentForm from '../components/Forms/TournamentForm'; import TournamentForm from '../components/Forms/TournamentForm';
@@ -11,8 +11,11 @@ import api, { WS_URL } from '../services/api';
export interface TournamentType { export interface TournamentType {
id: string | number; id: string | number;
name: string;
timestamp: string; timestamp: string;
[key: string]: any; type: string;
team_count: number;
court_count: number;
} }
interface OutletContextType { interface OutletContextType {
@@ -32,20 +35,22 @@ export default function Dashboard() {
const [showAllFuture, setShowAllFuture] = useState<boolean>(false); const [showAllFuture, setShowAllFuture] = useState<boolean>(false);
const navigate = useNavigate(); const navigate = useNavigate();
const loadDashboard = async () => { const loadDashboard = useCallback(async () => {
try { try {
const res: any = await api.get('/tournaments'); const res = await api.get<{ items?: TournamentType[] } | TournamentType[]>('/tournaments');
const list = Array.isArray(res) ? res : (res.items || []); const list = Array.isArray(res) ? res : (res?.items || []);
setTournaments(list); setTournaments(list as TournamentType[]);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}; }, []);
useEffect(() => { useEffect(() => {
setNavTitle('Dashboard'); setNavTitle('Dashboard');
setNavSubtitle(''); setNavSubtitle('');
loadDashboard(); // eslint-disable-next-line react-hooks/set-state-in-effect
void loadDashboard();
localStorage.removeItem('volley_view'); localStorage.removeItem('volley_view');
let ws: WebSocket; let ws: WebSocket;
@@ -63,7 +68,7 @@ export default function Dashboard() {
connect(); connect();
return () => { if (ws) ws.close(); }; return () => { if (ws) ws.close(); };
}, [setNavTitle, setNavSubtitle]); }, [setNavTitle, setNavSubtitle, loadDashboard]);
const handleEdit = (t: TournamentType) => { const handleEdit = (t: TournamentType) => {
setEditTarget(t); setEditTarget(t);
@@ -126,7 +131,7 @@ export default function Dashboard() {
</span> Live Events </span> Live Events
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <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: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)} {groups.live.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
</div> </div>
</section> </section>
)} )}
@@ -138,7 +143,7 @@ export default function Dashboard() {
{groups.future.length > 0 ? ( {groups.future.length > 0 ? (
<> <>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <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: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)} {futureShow.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
</div> </div>
{groups.future.length > 4 && ( {groups.future.length > 4 && (
<div className="mt-8 text-center"> <div className="mt-8 text-center">
@@ -160,7 +165,7 @@ export default function Dashboard() {
</button> </button>
{showPast && ( {showPast && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 opacity-75 hover:opacity-100 transition-opacity"> <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: string) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)} {groups.past.map(t => <DashCard key={t.id} t={t} isAdmin={isAdmin} onSelect={(id) => navigate(`/tournaments/${id}`)} onEdit={handleEdit} />)}
</div> </div>
)} )}
</section> </section>
+1 -1
View File
@@ -26,7 +26,7 @@ export default function Login() {
const res = await api.postForm<TokenResponse>('/auth/token', formData); const res = await api.postForm<TokenResponse>('/auth/token', formData);
localStorage.setItem('volleyToken', res.access_token); localStorage.setItem('volleyToken', res.access_token);
navigate('/'); navigate('/');
} catch (err) { } catch {
setError('Invalid credentials.'); setError('Invalid credentials.');
} finally { } finally {
setLoading(false); setLoading(false);
+10 -10
View File
@@ -1,5 +1,6 @@
// frontend/src/pages/Tournament.tsx // frontend/src/pages/Tournament.tsx
import { type SetData } from '../types';
import { CalendarDays, Loader2, Network, SlidersHorizontal } from 'lucide-react'; import { CalendarDays, Loader2, Network, SlidersHorizontal } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { useOutletContext, useParams } from 'react-router-dom'; import { useOutletContext, useParams } from 'react-router-dom';
@@ -134,9 +135,9 @@ export default function Tournament() {
}); });
}; };
const fetchData = async () => { const fetchData = React.useCallback(async () => {
try { try {
const res: any = await api.get(`/tournaments/${id}`); const res = await api.get<{ name: string, timestamp: string, matches: RawMatch[], courts: Court[], teams: Team[] }>(`/tournaments/${id}`);
setNavTitle(res.name); setNavTitle(res.name);
setNavSubtitle(new Date(res.timestamp).toLocaleDateString()); setNavSubtitle(new Date(res.timestamp).toLocaleDateString());
setMatches(processMatches(res.matches, res.courts, res.teams)); setMatches(processMatches(res.matches, res.courts, res.teams));
@@ -145,10 +146,10 @@ export default function Tournament() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [id, setNavTitle, setNavSubtitle]);
useEffect(() => { useEffect(() => {
fetchData(); void fetchData();
if (wsRef.current) return; if (wsRef.current) return;
const connect = () => { const connect = () => {
@@ -156,7 +157,7 @@ export default function Tournament() {
wsRef.current = ws; wsRef.current = ws;
ws.onmessage = (e: MessageEvent) => { ws.onmessage = (e: MessageEvent) => {
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === 'tournament_update' && msg.id === id) fetchData(); if (msg.type === 'tournament_update' && msg.id === id) void fetchData();
}; };
ws.onclose = () => { wsRef.current = null; }; ws.onclose = () => { wsRef.current = null; };
}; };
@@ -167,7 +168,7 @@ export default function Tournament() {
if (wsRef.current?.readyState === 1) wsRef.current.close(); if (wsRef.current?.readyState === 1) wsRef.current.close();
wsRef.current = null; wsRef.current = null;
}; };
}, [id, setNavTitle, setNavSubtitle]); }, [id, fetchData]);
const handleDeleteTournament = async (tId: string | number) => { const handleDeleteTournament = async (tId: string | number) => {
if (window.confirm("Purge this tournament?")) { if (window.confirm("Purge this tournament?")) {
@@ -194,8 +195,8 @@ export default function Tournament() {
<div className="flex-1 overflow-hidden relative print:overflow-visible print:h-auto print:block"> <div className="flex-1 overflow-hidden relative print:overflow-visible print:h-auto print:block">
{view === 'bracket' {view === 'bracket'
? <BracketView matches={matches} onMatchClick={setScoreMatch} /> ? <BracketView matches={matches} onMatchClick={(m) => setScoreMatch(m as ProcessedMatch)} />
: <ScheduleView schedule={matches} onMatchClick={setScoreMatch} /> : <ScheduleView schedule={matches} onMatchClick={(m) => setScoreMatch(m as ProcessedMatch)} />
} }
</div> </div>
@@ -209,9 +210,8 @@ export default function Tournament() {
await api.delete(`/tournaments/${id}/matches/${mid}/score?code=${encodeURIComponent(c || '')}`); await api.delete(`/tournaments/${id}/matches/${mid}/score?code=${encodeURIComponent(c || '')}`);
setScoreMatch(null); setScoreMatch(null);
}} }}
onSubmit={async (mid: string | number, s: any, c: string) => { onSubmit={async (mid: string | number, s: SetData[], c: string) => {
const method = scoreMatch.isFinished ? 'patch' : 'post'; const method = scoreMatch.isFinished ? 'patch' : 'post';
// @ts-ignore - Indexing api dynamically
await api[method](`/tournaments/${id}/matches/${mid}/score`, { sets: s, code: c }); await api[method](`/tournaments/${id}/matches/${mid}/score`, { sets: s, code: c });
setScoreMatch(null); setScoreMatch(null);
}} }}
+5 -10
View File
@@ -16,7 +16,7 @@ const api = {
request: async <T>( request: async <T>(
method: HttpMethod, method: HttpMethod,
url: string, url: string,
data: any = null, data: unknown = null,
isFormData: boolean = false isFormData: boolean = false
): Promise<T> => { ): Promise<T> => {
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
@@ -31,7 +31,7 @@ const api = {
}; };
if (data) { if (data) {
opts.body = isFormData ? data : JSON.stringify(data); opts.body = isFormData ? (data as FormData) : JSON.stringify(data);
} }
// Ensure clean URL concatenation // Ensure clean URL concatenation
@@ -56,15 +56,10 @@ const api = {
}, },
get: <T>(url: string) => api.request<T>('GET', url), get: <T>(url: string) => api.request<T>('GET', url),
post: <T>(url: string, data?: unknown) => api.request<T>('POST', url, data),
post: <T>(url: string, data?: any) => api.request<T>('POST', url, data),
postForm: <T>(url: string, data: FormData) => api.request<T>('POST', url, data, true), postForm: <T>(url: string, data: FormData) => api.request<T>('POST', url, data, true),
put: <T>(url: string, data?: unknown) => api.request<T>('PUT', url, data),
put: <T>(url: string, data?: any) => api.request<T>('PUT', url, data), patch: <T>(url: string, data?: unknown) => api.request<T>('PATCH', url, data),
patch: <T>(url: string, data?: any) => api.request<T>('PATCH', url, data),
delete: <T>(url: string) => api.request<T>('DELETE', url) delete: <T>(url: string) => api.request<T>('DELETE', url)
}; };
+11
View File
@@ -18,6 +18,12 @@ export interface MatchData {
p1_is_real: boolean; p1_is_real: boolean;
p2_is_real: boolean; p2_is_real: boolean;
winnerName: string | null; winnerName: string | null;
bracket_type?: string;
round_number?: number;
match_number?: number;
court_id?: string | number;
winner_team_id?: string | number | null; winner_team_id?: string | number | null;
p1_team_id?: string | number | null; p1_team_id?: string | number | null;
p2_team_id?: string | number | null; p2_team_id?: string | number | null;
@@ -26,3 +32,8 @@ export interface MatchData {
timestamp?: string; timestamp?: string;
start_time?: string; start_time?: string;
} }
export interface SetData {
p1: number | string;
p2: number | string;
}
+28
View File
@@ -0,0 +1,28 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
+4 -35
View File
@@ -1,38 +1,7 @@
{ {
"compilerOptions": { "files": [],
"target": "ES2020", "references": [
"useDefineForClassFields": true, { "path": "./tsconfig.app.json" },
"lib": [ { "path": "./tsconfig.node.json" }
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path Aliases */
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"src",
"vite.config.ts"
],
"exclude": [
"node_modules"
] ]
} }
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+6 -17
View File
@@ -1,21 +1,10 @@
// frontend/vite.config.ts // frontend/vite.config.ts
import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react'
import path from 'path'; import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vitest/config';
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss(),],
test: { })
globals: true,
environment: 'jsdom',
setupFiles: './vitest.setup.ts',
css: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
-3
View File
@@ -1,3 +0,0 @@
// frontend/vitest.setup.js
import '@testing-library/jest-dom/vitest';