Files
ultimate-frisbee/drills/flat-stack-presejpacky/src/App.tsx

226 lines
12 KiB
TypeScript

import { useState, useEffect } from 'react';
import { FieldCanvas } from './components/FieldCanvas';
import { DrillTimeline } from './components/DrillTimeline';
import { STEPS, getPositionsForStep } from './lib/drill';
import { Play, Pause, RotateCcw, LayoutTemplate, Info, X, SkipBack, SkipForward } from 'lucide-react';
import './index.css';
export default function App() {
const [cycle, setCycle] = useState(0);
const [step, setStep] = useState(0);
const [playing, setPlaying] = useState(false);
const [showInfo, setShowInfo] = useState(false);
useEffect(() => {
if (!playing) return;
const currentDuration = STEPS[step].duration;
// Automatically transition to the next step
const timer = setTimeout(() => {
if (step === STEPS.length - 1) {
setStep(0);
setCycle(c => c + 1);
} else {
setStep(s => s + 1);
}
}, currentDuration);
return () => clearTimeout(timer);
}, [playing, step, cycle]);
// Derive all rendered positions mathematically from cycle and step
const { pos, discX, discY, markX, markY, markAngle } = getPositionsForStep(cycle, step);
return (
<div className="flex flex-col h-screen w-full bg-slate-900 text-slate-100 font-sans overflow-hidden">
{/* Header Navigation */}
<header className="flex items-center justify-between px-6 lg:px-8 py-4 bg-slate-800/50 border-b border-slate-700 shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-indigo-600 rounded-lg flex items-center justify-center shadow-lg shadow-indigo-500/20">
<LayoutTemplate className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold tracking-tight text-white flex items-center gap-2">
Half-Flat Break-Side Pivot
<button
onClick={() => setShowInfo(true)}
className="ml-2 p-1 text-slate-400 hover:text-indigo-400 hover:bg-slate-800 rounded-full transition-colors"
title="Drill Description"
>
<Info className="w-5 h-5" />
</button>
</h1>
<p className="text-[10px] sm:text-xs text-slate-400 uppercase tracking-widest leading-none mt-1">Tactical Drill Simulator</p>
</div>
</div>
</header>
{/* Main Workspace */}
<main className="flex flex-1 overflow-hidden relative">
{/* Sidebar Controls */}
<aside className="w-80 border-r border-slate-700 bg-slate-800/30 p-6 flex-col gap-6 overflow-y-auto shrink-0 hidden lg:flex custom-scrollbar">
<section className="bg-slate-800/50 border border-slate-700/50 p-4 rounded-xl shadow-inner">
<h3 className="text-[10px] font-bold text-indigo-400 uppercase mb-2 tracking-widest">Current Action Focus</h3>
<h4 className="text-base font-semibold text-white mb-2">{STEPS[step].name}</h4>
<p className="text-sm text-slate-300 leading-relaxed">
{STEPS[step].description}
</p>
</section>
<section className="flex-1 flex flex-col min-h-0">
<h3 className="text-xs font-bold text-slate-500 uppercase mb-4 flex justify-between items-center">
Active Rotation
<span className="px-2 py-0.5 bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 font-mono text-[10px] rounded">
CYCLE {cycle + 1}
</span>
</h3>
<DrillTimeline currentStep={step} cycle={cycle} />
</section>
</aside>
{/* Tactical Field Visualizer */}
<div className="flex-1 relative bg-slate-950 p-4 md:p-12 flex flex-col items-center justify-center overflow-hidden">
<div className="w-full flex-1 max-h-[800px] flex items-center justify-center p-4">
<div className="w-full max-w-[500px] aspect-[5/7] h-full max-h-full">
<FieldCanvas
pos={pos}
discX={discX}
discY={discY}
markX={markX}
markY={markY}
markAngle={markAngle}
stepDuration={STEPS[step].duration}
step={step}
/>
</div>
</div>
{/* Overlay Legend */}
<div className="absolute bottom-6 md:bottom-12 right-6 md:right-12 bg-slate-800/80 backdrop-blur-md border border-slate-700 p-3 sm:p-4 rounded-xl flex gap-4 sm:gap-6 shadow-xl z-20">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-full shadow-[0_0_8px_rgba(59,130,246,0.5)]"></div>
<span className="text-[10px] uppercase font-bold text-slate-300 tracking-wider">Handler</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-slate-400 rounded-full border border-slate-300"></div>
<span className="text-[10px] uppercase font-bold text-slate-300 tracking-wider">Cutter</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-red-600 rounded-sm rotate-45"></div>
<span className="text-[10px] uppercase font-bold text-slate-300 tracking-wider">Defense</span>
</div>
</div>
</div>
{/* Info Modal Overlay */}
{showInfo && (
<div className="absolute inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm">
<div className="bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl max-w-2xl w-full flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between p-6 border-b border-slate-800 bg-slate-800/30">
<h2 className="text-xl font-bold text-white flex items-center gap-2">
<Info className="w-6 h-6 text-indigo-400" /> Drill Overview
</h2>
<button
onClick={() => setShowInfo(false)}
className="p-2 bg-slate-800 hover:bg-slate-700 rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<div className="p-6 overflow-y-auto max-h-[70vh] flex flex-col gap-4 text-slate-300">
<p className="text-base leading-relaxed">
The <strong className="text-white">Isolation Break Drill</strong> focuses on exploiting the defense using handler movement to open up the break side from a half-flatstack setup.
</p>
<div className="bg-slate-800/50 p-4 rounded-xl border border-slate-700">
<h3 className="font-semibold text-indigo-300 mb-3 uppercase tracking-wider text-xs">Sequence of Motions</h3>
<ol className="list-decimal list-outside ml-4 space-y-3 text-sm">
<li><strong className="text-slate-100">Initiation:</strong> The breakside handler drives hard to the open side, threatening a give-and-go cut. The mark tracks this movement.</li>
<li><strong className="text-slate-100">The Fake:</strong> The central handler (with the disc) fakes a throw to the open side, forcing the mark to bite and shift their weight fully into the open lane.</li>
<li><strong className="text-slate-100">The Pivot:</strong> Exploiting the mark's overcommitment, the central handler rapidly pivots back to the breakside, stepping around the off-balance defender.</li>
<li><strong className="text-slate-100">The Cut:</strong> Synchronized perfectly with the thrower's pivot, the cutter on the line (breakside) makes a hard cut parallel to the sideline towards the disc.</li>
<li><strong className="text-slate-100">The Throw:</strong> The handler delivers a breakside pass into space, allowing the cutter to catch it in stride and continue advancing upfield.</li>
</ol>
</div>
<div className="bg-emerald-900/20 border border-emerald-500/30 p-4 rounded-xl">
<p className="text-sm text-emerald-200 leading-relaxed">
<span className="font-bold text-emerald-400">Coaching Tip:</span> Ensure the line cutter doesn't start their cut too early. They must wait for the central handler's eyes to snap back to the break side before initiating the horizontal cut so they arrive exactly on time.
</p>
</div>
</div>
</div>
</div>
)}
</main>
{/* Playback Control Bar */}
<footer className="h-20 lg:h-24 bg-slate-900 border-t border-slate-700 flex items-center px-6 lg:px-12 gap-6 lg:gap-8 shrink-0 z-10">
<div className="flex items-center gap-3 md:gap-4">
<button
onClick={() => {
setPlaying(false);
if (step > 0) setStep(step - 1);
else if (cycle > 0) { setCycle(cycle - 1); setStep(STEPS.length - 1); }
}}
className="w-10 h-10 flex items-center justify-center bg-slate-800 rounded-full border border-slate-700 hover:text-indigo-400 transition-colors text-slate-400 hover:border-indigo-500/50"
title="Previous Step"
>
<SkipBack className="w-4 h-4" />
</button>
<button
onClick={() => setPlaying(!playing)}
className="w-14 h-14 flex items-center justify-center bg-indigo-600 rounded-full shadow-lg shadow-indigo-500/30 hover:scale-105 active:scale-95 transition-all text-white border border-indigo-400/50"
title={playing ? "Pause" : "Play"}
>
{playing ? <Pause className="w-6 h-6 fill-current" /> : <Play className="w-6 h-6 fill-current ml-1" />}
</button>
<button
onClick={() => {
setPlaying(false);
if (step < STEPS.length - 1) setStep(step + 1);
else { setCycle(cycle + 1); setStep(0); }
}}
className="w-10 h-10 flex items-center justify-center bg-slate-800 rounded-full border border-slate-700 hover:text-indigo-400 transition-colors text-slate-400 hover:border-indigo-500/50"
title="Next Step"
>
<SkipForward className="w-4 h-4" />
</button>
<button
onClick={() => { setStep(0); setCycle(0); setPlaying(false); }}
className="w-10 h-10 hidden sm:flex items-center justify-center bg-slate-800 rounded-full border border-slate-700 hover:text-indigo-400 transition-colors text-slate-400 hover:border-indigo-500/50 ml-2"
title="Restart Drill"
>
<RotateCcw className="w-4 h-4" />
</button>
</div>
<div className="flex-1 flex flex-col gap-2 mx-2">
<div className="flex justify-between text-[10px] font-mono text-slate-500 uppercase tracking-widest px-1">
<span>{String((step * 0.45).toFixed(2)).padStart(5, '0')}</span>
<span className="font-semibold text-indigo-400 text-xs hidden sm:inline">{STEPS[step]?.name}</span>
<span>{String(((STEPS.length - 1) * 0.45).toFixed(2)).padStart(5, '0')}</span>
</div>
<div className="relative h-2 w-full bg-slate-800 rounded-full overflow-hidden border border-slate-700/50">
<div
className="absolute top-0 left-0 h-full bg-indigo-500 transition-all duration-300 ease-linear shadow-[0_0_10px_rgba(99,102,241,0.8)]"
style={{ width: `${Math.max(2, ((step) / Math.max(1, STEPS.length - 1)) * 100)}%` }}
></div>
<div className="absolute top-0 w-0.5 h-full bg-white shadow-[0_0_5px_white] transition-all duration-300 ease-linear" style={{ left: `calc(${Math.max(2, ((step) / Math.max(1, STEPS.length - 1)) * 100)}% - 2px)` }}></div>
</div>
</div>
<div className="hidden md:flex items-center gap-6 text-slate-400">
<div className="flex flex-col items-end">
<span className="text-[10px] font-bold uppercase tracking-tighter text-slate-500">Speed</span>
<span className="text-xs font-mono text-indigo-400 font-semibold">1.0x</span>
</div>
</div>
</footer>
</div>
);
}