Compare commits
Vendored
+2
-1
@@ -3,5 +3,6 @@
|
||||
"backend"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python-envs.defaultEnvManager": "ms-python.python:venv"
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
Generated
+4065
-1
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user