137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useCallback, useRef } from "react";
|
|
|
|
interface StepControlsProps {
|
|
currentStep: number;
|
|
totalSteps: number;
|
|
isPlaying: boolean;
|
|
onStepForward: () => void;
|
|
onStepBack: () => void;
|
|
onTogglePlay: () => void;
|
|
onReset: () => void;
|
|
canStepForward: boolean;
|
|
canStepBack: boolean;
|
|
playIntervalMs?: number;
|
|
}
|
|
|
|
export function StepControls({
|
|
currentStep,
|
|
totalSteps,
|
|
isPlaying,
|
|
onStepForward,
|
|
onStepBack,
|
|
onTogglePlay,
|
|
onReset,
|
|
canStepForward,
|
|
canStepBack,
|
|
playIntervalMs = 950,
|
|
}: StepControlsProps) {
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
// Auto-play timer
|
|
useEffect(() => {
|
|
if (isPlaying && canStepForward) {
|
|
timerRef.current = setInterval(() => {
|
|
onStepForward();
|
|
}, playIntervalMs);
|
|
}
|
|
return () => {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
};
|
|
}, [isPlaying, canStepForward, onStepForward, playIntervalMs]);
|
|
|
|
// Keyboard shortcuts
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
const el = e.target as HTMLElement;
|
|
const tag = el.tagName;
|
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
|
|
if (el.closest("button, [role='button'], a")) return;
|
|
|
|
if (e.key === "ArrowRight" || e.key === " ") {
|
|
e.preventDefault();
|
|
if (canStepForward) onStepForward();
|
|
}
|
|
if (e.key === "ArrowLeft") {
|
|
e.preventDefault();
|
|
if (canStepBack) onStepBack();
|
|
}
|
|
if (e.key.toLowerCase() === "p") onTogglePlay();
|
|
if (e.key.toLowerCase() === "r") onReset();
|
|
},
|
|
[canStepForward, canStepBack, onStepForward, onStepBack, onTogglePlay, onReset],
|
|
);
|
|
|
|
useEffect(() => {
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
}, [handleKeyDown]);
|
|
|
|
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Progress bar */}
|
|
{totalSteps > 0 && (
|
|
<div className="mx-auto max-w-md">
|
|
<div className="mb-1.5 flex items-center justify-between text-xs text-muted">
|
|
<span>Step {currentStep} of {totalSteps}</span>
|
|
<span>{Math.round(progress)}%</span>
|
|
</div>
|
|
<div className="h-1.5 overflow-hidden rounded-full bg-border/60">
|
|
<div
|
|
className="h-full rounded-full bg-gradient-to-r from-unit-1 to-unit-4 transition-all duration-300"
|
|
style={{ width: `${progress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Controls */}
|
|
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
<button
|
|
onClick={onStepBack}
|
|
disabled={!canStepBack || isPlaying}
|
|
className="rounded-xl border border-border/60 bg-surface px-4 py-2.5 text-sm font-semibold text-foreground shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
|
|
>
|
|
◀ Back
|
|
</button>
|
|
<button
|
|
onClick={onTogglePlay}
|
|
disabled={!canStepForward && !isPlaying}
|
|
className="min-w-[110px] rounded-xl bg-correct px-5 py-2.5 text-sm font-semibold text-white shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
|
|
>
|
|
{isPlaying ? "⏸ Pause" : "▶ Play"}
|
|
</button>
|
|
<button
|
|
onClick={onStepForward}
|
|
disabled={!canStepForward || isPlaying}
|
|
className="rounded-xl bg-foreground px-5 py-2.5 text-sm font-semibold text-background shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)] hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-35 disabled:hover:translate-y-0 disabled:hover:shadow-[var(--shadow-sm)]"
|
|
>
|
|
Next Step ▶
|
|
</button>
|
|
<button
|
|
onClick={onReset}
|
|
className="rounded-xl border border-incorrect/30 bg-surface px-4 py-2.5 text-sm font-semibold text-incorrect shadow-[var(--shadow-sm)] transition-all duration-200 hover:bg-incorrect hover:text-white hover:shadow-[var(--shadow-md)] hover:-translate-y-px"
|
|
>
|
|
↺ Reset
|
|
</button>
|
|
</div>
|
|
|
|
{/* Keyboard hint */}
|
|
<p className="text-center text-[0.7rem] text-muted/50">
|
|
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">Space</kbd>{" / "}
|
|
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">→</kbd>{" "}
|
|
next{" · "}
|
|
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">←</kbd>{" "}
|
|
back{" · "}
|
|
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">P</kbd>{" "}
|
|
play{" · "}
|
|
<kbd className="rounded border border-border/60 bg-background px-1 py-px font-mono text-[0.6rem]">R</kbd>{" "}
|
|
reset
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|