Added refs
This commit is contained in:
Generated
+981
-561
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-svgr": "^5.0.0",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor">
|
||||
<path d="M400 64c8.8 0 16 7.2 16 16 0 17.7 14.3 32 32 32s32-14.3 32-32c0-8.8 7.2-16 16-16l112 0c17.7 0 32 14.3 32 32l0 70.3c0 15-10.4 28-25.1 31.2L413.8 242.2c1.4 9.7 2.2 19.6 2.2 29.8 0 114.9-93.1 208-208 208S0 386.9 0 272 93.1 64 208 64l192 0zM208 192a80 80 0 1 0 0 160 80 80 0 1 0 0-160z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -3,6 +3,7 @@
|
||||
import { Check } from 'lucide-react';
|
||||
import { type MatchData } from '../../types';
|
||||
import { printName, stringToColor } from '../../utils/helpers';
|
||||
import WhistleIcon from "../../assets/whistle.svg?react"
|
||||
|
||||
interface MatchCardProps {
|
||||
match: MatchData;
|
||||
@@ -21,6 +22,8 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
|
||||
? 'cursor-pointer hover:shadow-md hover:-translate-y-0.5'
|
||||
: 'cursor-default opacity-100';
|
||||
|
||||
const refName = match.ref_team?.name || match.ref_label;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`match-${match.id}`}
|
||||
@@ -43,7 +46,7 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-1.5">
|
||||
<div className="p-2 space-y-1.5 flex-1 flex flex-col justify-center">
|
||||
{[
|
||||
{ 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 }
|
||||
@@ -66,6 +69,14 @@ export default function MatchCard({ match, onClick }: MatchCardProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* --- FLOATING REF BADGE --- */}
|
||||
{refName && !isFinished && (
|
||||
<div className="absolute -bottom-2.5 left-1/2 -translate-x-1/2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-400 text-[8px] font-black uppercase tracking-widest px-2.5 py-0.5 rounded-full shadow-sm whitespace-nowrap z-20 flex items-center gap-1.5 print:hidden max-w-[90%]">
|
||||
<WhistleIcon className="text-orange-500 shrink-0" width={12} height={12} />
|
||||
<span className="truncate">{refName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { CheckCircle, Pencil, Plus, Trophy } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { type MatchData } from '../../types';
|
||||
import { printName, stringToColor } from '../../utils/helpers';
|
||||
import WhistleIcon from "../../assets/whistle.svg?react"
|
||||
|
||||
interface ScheduleRowProps {
|
||||
match: MatchData;
|
||||
@@ -14,9 +15,10 @@ interface ScheduleRowProps {
|
||||
export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: ScheduleRowProps) {
|
||||
const courtColor = stringToColor(m.court);
|
||||
const isFinished = m.isFinished;
|
||||
const refName = m.ref_team?.name || m.ref_label;
|
||||
|
||||
return (
|
||||
<div className="transition-colors bg-white dark:bg-zinc-900 print:bg-white! p-4 print:p-3 rounded-2xl print:rounded-none border border-zinc-300 dark:border-zinc-800 print:border-b! print:border-x-0! print:border-t-0! print:border-zinc-300! shadow-sm print:shadow-none! flex items-center justify-between hover:border-orange-500/30">
|
||||
<div className="relative mb-5 transition-colors bg-white dark:bg-zinc-900 print:bg-white! p-4 print:p-3 rounded-2xl print:rounded-none border border-zinc-300 dark:border-zinc-800 print:border-b! print:border-x-0! print:border-t-0! print:border-zinc-300! shadow-sm print:shadow-none! flex items-center justify-between hover:border-orange-500/30">
|
||||
<div className="flex gap-4 md:gap-6 print:gap-6 flex-1 min-w-0">
|
||||
<div className="flex flex-col gap-1 items-center shrink-0" style={{ minWidth: badgeWidth }}>
|
||||
<div className="text-lg md:text-xl print:text-xl font-black font-mono text-zinc-900 dark:text-white print:text-black!">{m.time}</div>
|
||||
@@ -42,7 +44,10 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="transition-colors hidden md:block print:block text-tiny font-black bg-zinc-100 dark:bg-zinc-800 print:bg-transparent! text-zinc-400 print:text-zinc-500! px-2 py-1 rounded w-fit">Match #{m.number}</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-0.5 md:gap-2 mt-0.5 md:mt-1">
|
||||
<div className="transition-colors hidden md:block print:block text-tiny font-black bg-zinc-100 dark:bg-zinc-800 print:bg-transparent! text-zinc-400 print:text-zinc-500! px-2 py-1 rounded w-fit">Match #{m.number}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,19 +59,14 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
|
||||
className="flex flex-col justify-between items-center md:justify-center md:items-end hover:bg-zinc-50 dark:hover:bg-zinc-800 p-1.5 md:p-2 rounded-xl transition group/btn min-w-8 md:min-w-20 border border-transparent hover:border-zinc-200 dark:hover:border-zinc-700"
|
||||
title="Edit Score"
|
||||
>
|
||||
{/* --- MOBILE VERTICAL STACK --- */}
|
||||
<div className={`${m.winnerName === m.p1 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}>
|
||||
{m.p1_sets}
|
||||
</div>
|
||||
|
||||
{/* Clean minimal vertical line for mobile */}
|
||||
<div className="w-0.5 h-3 bg-zinc-200 dark:bg-zinc-700 rounded-full md:hidden my-1" />
|
||||
|
||||
<div className={`${m.winnerName === m.p2 ? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-zinc-50'} px-2 py-0.5 rounded text-[10px] font-black font-mono border border-zinc-200 dark:border-zinc-700 md:hidden`}>
|
||||
{m.p2_sets}
|
||||
</div>
|
||||
|
||||
{/* --- DESKTOP VIEW (Default + Hover Edit) --- */}
|
||||
<div className="hidden md:flex group-hover/btn:hidden flex-col items-end">
|
||||
<div className="text-orange-500 font-black text-[10px] uppercase flex items-center gap-1">
|
||||
<CheckCircle size={12} strokeWidth={3} /> Finished
|
||||
@@ -102,6 +102,13 @@ export default function ScheduleRow({ match: m, onMatchClick, badgeWidth }: Sche
|
||||
{isFinished ? m.p2_sets : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{refName && !isFinished && (
|
||||
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-300 text-[9px] md:text-[10px] font-black uppercase tracking-widest px-3 md:px-4 py-1 rounded-full shadow-md whitespace-nowrap z-20 flex items-center gap-1.5 print:hidden max-w-[90%]">
|
||||
<WhistleIcon className="text-orange-500 shrink-0" width={12} height={12} />
|
||||
<span className="truncate">{refName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default function ScheduleView({ schedule, onMatchClick }: ScheduleViewPro
|
||||
const badgeWidth = Math.max(80, longestCourt.length * 10);
|
||||
|
||||
const filteredAndSorted = schedule
|
||||
.filter(m => (m.p1 + m.p2 + m.number + `Match #${m.number}`).toLowerCase().includes(filter.toLowerCase()))
|
||||
.filter(m => (m.p1 + m.p2 + m.number + `Match #${m.number}` + m.ref_label).toLowerCase().includes(filter.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
const timeA = a.start_time || a.timestamp || a.time || "";
|
||||
const timeB = b.start_time || b.timestamp || b.time || "";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { type SetData } from '../../types';
|
||||
import { Clock, Eraser, MapPin, Trophy } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Modal from '../UI/Modal';
|
||||
|
||||
import WhistleIcon from "../../assets/whistle.svg?react"
|
||||
|
||||
interface MatchData {
|
||||
id: string | number;
|
||||
@@ -15,6 +15,8 @@ interface MatchData {
|
||||
p1_label?: string;
|
||||
p2?: string;
|
||||
p2_label?: string;
|
||||
ref_label?: string;
|
||||
ref_team?: { name: string };
|
||||
isFinished: boolean;
|
||||
sets?: SetData[];
|
||||
}
|
||||
@@ -30,6 +32,7 @@ const ScoreForm = ({ match, isAuthenticated, onSubmit, onClear }: ScoreFormProps
|
||||
const [sets, setSets] = useState<SetData[]>(match.sets && match.sets.length ? match.sets : [{ p1: '', p2: '' }]);
|
||||
const [code, setCode] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const refName = match.ref_team?.name || match.ref_label;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@@ -52,17 +55,29 @@ const ScoreForm = ({ match, isAuthenticated, onSubmit, onClear }: ScoreFormProps
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Top Info Bar */}
|
||||
<div className="flex justify-center items-center gap-4 bg-zinc-50 dark:bg-zinc-950 p-3 rounded-lg border border-gray-200 dark:border-zinc-800 shadow-sm transition-colors">
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-600 dark:text-zinc-300">
|
||||
<Clock className="text-orange-500" size={18} />
|
||||
<span>{match.time || "10:00"}</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-zinc-300 dark:bg-zinc-800" />
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-900 dark:text-white">
|
||||
<MapPin className="text-orange-500" size={18} />
|
||||
<span>{match.court || "TBD"}</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Time & Court Row */}
|
||||
<div className="flex justify-center items-center gap-4 bg-zinc-50 dark:bg-zinc-950 p-3 rounded-lg border border-gray-200 dark:border-zinc-800 shadow-sm transition-colors">
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-600 dark:text-zinc-300">
|
||||
<Clock className="text-orange-500" size={16} />
|
||||
<span>{match.time || "10:00"}</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-zinc-300 dark:bg-zinc-800" />
|
||||
<div className="flex items-center gap-2 text-sm font-mono text-zinc-900 dark:text-white">
|
||||
<MapPin className="text-orange-500" size={16} />
|
||||
<span>{match.court || "TBD"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dedicated Ref Row (Always visible) */}
|
||||
{refName && (
|
||||
<div className="flex justify-center items-center gap-2 bg-orange-50 dark:bg-orange-900/10 p-2.5 rounded-lg border border-orange-100 dark:border-orange-900/30">
|
||||
<WhistleIcon className="text-orange-500 shrink-0" width={14} height={14} />
|
||||
<span className="text-xs font-bold text-orange-700 dark:text-orange-500 uppercase tracking-wide truncate">
|
||||
{refName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -31,6 +31,8 @@ export interface MatchData {
|
||||
loser_next_match_id?: string | number | null;
|
||||
timestamp?: string;
|
||||
start_time?: string;
|
||||
ref_label?: string;
|
||||
ref_team?: { id: string | number; name: string };
|
||||
}
|
||||
|
||||
export interface SetData {
|
||||
|
||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
// frontend/src/vite-env.d.ts
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
@@ -4,11 +4,13 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
svgr(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg'],
|
||||
|
||||
Reference in New Issue
Block a user