Typscript final
This commit is contained in:
+59
-2
@@ -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...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -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_]' }],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
// frontend/global.d.ts
|
|
||||||
|
|
||||||
declare module '*.css';
|
|
||||||
Generated
+269
-262
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// frontend/vitest.setup.js
|
|
||||||
|
|
||||||
import '@testing-library/jest-dom/vitest';
|
|
||||||
Reference in New Issue
Block a user