3 Commits
10 changed files with 4202 additions and 45 deletions
+2 -1
View File
@@ -3,5 +3,6 @@
"backend"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"python-envs.defaultEnvManager": "ms-python.python:venv"
}
+4
View File
@@ -1,5 +1,9 @@
ADMIN_USER=admin
ADMIN_PASSWORD=admin
REF_USER=ref
REF_PASSWORD=ref
SECRET_KEY=PLEASE_REPLACE_ME_WITH_A_SECRET_KEY
# Optional
+2 -2
View File
@@ -5,7 +5,7 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="src/assets/favicon.svg" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>VolleyManager</title>
@@ -13,7 +13,7 @@
<body class="transition bg-zinc-50 dark:bg-zinc-950">
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+4065 -1
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -7,7 +7,7 @@
"dev": "vite --host",
"build": "tsc -b && vite build",
"lint": "tsc -b && eslint .",
"preview": "vite preview",
"preview": "vite preview --host",
"test": "vitest"
},
"dependencies": {
@@ -38,6 +38,7 @@
"tailwindcss": "^4.1.18",
"typescript-eslint": "^8.57.0",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0",
"vitest": "^4.0.18"
}
}

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+94 -39
View File
@@ -1,6 +1,7 @@
// frontend/src/components/Bracket/BracketView.tsx
import React, { useEffect, useRef, useState } from 'react';
import { ZoomIn, ZoomOut } from 'lucide-react';
import { type MatchData } from '../../types';
import Podium from "../Tournament/Podium";
import MatchCard from "./MatchCard";
@@ -13,10 +14,19 @@ interface BracketViewProps {
export default function BracketView({ matches, onMatchClick }: BracketViewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [lines, setLines] = useState<React.ReactElement[]>([]);
const [zoom, setZoom] = useState<number>(1);
const [contentSize, setContentSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const draw = () => {
if (!containerRef.current) return;
// Measure the unscaled bracket size to fix scrollbars later
setContentSize({
width: containerRef.current.scrollWidth,
height: containerRef.current.scrollHeight
});
const container = containerRef.current.getBoundingClientRect();
const newLines: React.ReactElement[] = [];
@@ -26,9 +36,13 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
const eEl = document.getElementById(`match-${m.winner_next_match_id}`);
if (sEl && eEl) {
const r1 = sEl.getBoundingClientRect(), r2 = eEl.getBoundingClientRect();
const sx = r1.right - container.left, sy = r1.top + r1.height / 2 - container.top;
const ex = r2.left - container.left, ey = r2.top + r2.height / 2 - container.top;
const sx = (r1.right - container.left) / zoom;
const sy = (r1.top + r1.height / 2 - container.top) / zoom;
const ex = (r2.left - container.left) / zoom;
const ey = (r2.top + r2.height / 2 - container.top) / zoom;
const c1 = sx + (ex - sx) / 2;
newLines.push(
<path key={`w-${m.id}`} d={`M ${sx} ${sy} C ${c1} ${sy}, ${c1} ${ey}, ${ex} ${ey}`} className="stroke-zinc-400/70 dark:stroke-zinc-700/70 print:stroke-zinc-400! fill-none stroke-[2px]" />
);
@@ -42,9 +56,13 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
const eEl = document.getElementById(`match-${targetMatch.id}`);
if (sEl && eEl) {
const r1 = sEl.getBoundingClientRect(), r2 = eEl.getBoundingClientRect();
const sx = r1.right - container.left, sy = r1.top + r1.height / 2 - container.top;
const ex = r2.left - container.left, ey = r2.top + r2.height / 2 - container.top;
const sx = (r1.right - container.left) / zoom;
const sy = (r1.top + r1.height / 2 - container.top) / zoom;
const ex = (r2.left - container.left) / zoom;
const ey = (r2.top + r2.height / 2 - container.top) / zoom;
const c1 = sx + (ex - sx) / 2;
newLines.push(
<path key={`l-${m.id}`} strokeDasharray="6 6" d={`M ${sx} ${sy} C ${c1} ${sy}, ${c1} ${ey}, ${ex} ${ey}`} className="stroke-zinc-300 dark:stroke-zinc-700 print:stroke-zinc-400! fill-none stroke-[1.5px]" />
);
@@ -55,7 +73,7 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
setLines(newLines);
};
const t = setTimeout(draw, 100);
const t = setTimeout(draw, 50);
window.addEventListener('resize', draw);
const handlePrint = () => { draw(); setTimeout(draw, 100); };
@@ -71,7 +89,7 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
window.removeEventListener('beforeprint', handlePrint);
window.removeEventListener('afterprint', draw);
};
}, [matches]);
}, [matches, zoom]);
const renderRound = (list: MatchData[]) => {
const rounds: Record<number, MatchData[]> = {};
@@ -109,44 +127,81 @@ export default function BracketView({ matches, onMatchClick }: BracketViewProps)
}
return (
<div className="transition-colors w-full h-full overflow-auto print:overflow-visible print:h-auto print:w-auto bg-zinc-50 dark:bg-zinc-950 bg-[radial-gradient(var(--color-zinc-300)_1px,transparent_1px)] dark:bg-[radial-gradient(var(--color-zinc-800)_1px,transparent_1px)] bg-size-[20px_20px] print:bg-white! print:bg-none!">
<style>
{`@media print {
@page { size: landscape; margin: 0.5cm; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: white !important; }
}`}
</style>
<div className="relative w-full h-full flex flex-col overflow-hidden bg-zinc-50 dark:bg-zinc-950 print:bg-white!">
<div ref={containerRef} className="relative min-w-max min-h-full p-12 flex gap-20 items-center">
<svg className="absolute inset-0 w-full h-full pointer-events-none z-0 print:overflow-visible">
{lines}
</svg>
{/* FLOATING ZOOM CONTROLS (Moved to top-6 to completely avoid theme button) */}
<div className="absolute top-6 right-6 flex flex-col gap-3 z-50 print:hidden">
<button
onClick={() => setZoom(z => Math.min(1, z + 0.1))}
disabled={zoom >= 1}
className="p-3 bg-white dark:bg-zinc-800 rounded-full shadow-lg shadow-black/5 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-300 disabled:opacity-30 hover:text-orange-500 hover:border-orange-500 transition active:scale-90"
title="Zoom In"
>
<ZoomIn size={20} strokeWidth={2.5} />
</button>
<button
onClick={() => setZoom(z => Math.max(0.3, z - 0.1))}
disabled={zoom <= 0.3}
className="p-3 bg-white dark:bg-zinc-800 rounded-full shadow-lg shadow-black/5 border border-zinc-200 dark:border-zinc-700 text-zinc-600 dark:text-zinc-300 disabled:opacity-30 hover:text-orange-500 hover:border-orange-500 transition active:scale-90"
title="Zoom Out"
>
<ZoomOut size={20} strokeWidth={2.5} />
</button>
</div>
<div className="flex flex-col gap-24">
<div className="relative">
<div className="absolute -top-8 left-0 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 print:text-black!">Winners Bracket</div>
<div className="flex gap-20">{renderRound(displayWb)}</div>
</div>
{isDoubleElim && (
<div className="relative pt-8 border-t border-dashed border-zinc-300 dark:border-zinc-800 print:border-zinc-400!">
<div className="absolute top-4 left-0 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 print:text-black!">Losers Bracket</div>
<div className="flex gap-20 mt-4">{renderRound(lb)}</div>
</div>
)}
</div>
<div className="flex-1 overflow-auto bg-[radial-gradient(var(--color-zinc-300)_1px,transparent_1px)] dark:bg-[radial-gradient(var(--color-zinc-800)_1px,transparent_1px)] bg-size-[20px_20px] print:bg-none!">
<style>
{`@media print {
@page { size: landscape; margin: 0.5cm; }
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; background: white !important; }
}`}
</style>
<div className="flex flex-col justify-center gap-6 z-10">
{displayFinals.length > 0 && (
<div className="relative flex flex-col gap-6">
<div className="absolute -top-10 left-1/2 -translate-x-1/2 text-[10px] font-black uppercase bg-orange-100 dark:bg-orange-900/30 text-orange-600 print:bg-transparent! print:border-black! print:text-black! px-4 py-1.5 rounded-full border border-orange-200 dark:border-orange-800 shadow-sm whitespace-nowrap">
Championship
{/* SIZER WRAPPER (Dynamically scales width/height to fix the scrollbar ghost space) */}
<div style={{
width: contentSize.width ? `${contentSize.width * zoom}px` : 'max-content',
height: contentSize.height ? `${contentSize.height * zoom}px` : 'max-content'
}}>
{/* SCALING WRAPPER */}
<div
className="origin-top-left print:transform-none! w-max h-max"
style={{ transform: `scale(${zoom})` }}
>
{/* Added pt-16 md:pt-20 to ensure absolute titles aren't clipped
*/}
<div ref={containerRef} className="relative min-w-max min-h-full pt-16 md:pt-20 px-6 md:px-12 pb-40 md:pb-32 flex gap-12 md:gap-20 items-center">
<svg className="absolute inset-0 w-full h-full pointer-events-none z-0 print:overflow-visible">
{lines}
</svg>
<div className="flex flex-col gap-24">
<div className="relative">
<div className="absolute -top-8 left-0 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 print:text-black!">Winners Bracket</div>
<div className="flex gap-20">{renderRound(displayWb)}</div>
</div>
{isDoubleElim && (
<div className="relative pt-8 border-t border-dashed border-zinc-300 dark:border-zinc-800 print:border-zinc-400!">
<div className="absolute top-4 left-0 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-400 print:text-black!">Losers Bracket</div>
<div className="flex gap-20 mt-4">{renderRound(lb)}</div>
</div>
)}
</div>
<div className="flex flex-col justify-center gap-6 z-10">
{displayFinals.length > 0 && (
<div className="relative flex flex-col gap-6">
<div className="absolute -top-10 left-1/2 -translate-x-1/2 text-[10px] font-black uppercase bg-orange-100 dark:bg-orange-900/30 text-orange-600 print:bg-transparent! print:border-black! print:text-black! px-4 py-1.5 rounded-full border border-orange-200 dark:border-orange-800 shadow-sm whitespace-nowrap">
Championship
</div>
{displayFinals.map(m => <MatchCard key={m.id} match={m} onClick={onMatchClick} />)}
</div>
)}
</div>
<div className="flex flex-col justify-center gap-6 z-10">
<Podium matches={matches} />
</div>
{displayFinals.map(m => <MatchCard key={m.id} match={m} onClick={onMatchClick} />)}
</div>
)}
</div>
<div className="flex flex-col justify-center gap-6 z-10">
<Podium matches={matches} />
</div>
</div>
</div>
</div>
+33 -1
View File
@@ -3,11 +3,43 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
tailwindcss()
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg'],
manifest: {
name: 'VolleyManager',
short_name: 'VolleyManager',
description: 'Tournament Operations Manager',
theme_color: '#09090b',
background_color: '#fafafa',
display: 'standalone',
orientation: 'portrait-primary',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
}
})
],
test: {
globals: true,