Gold is the primary in both themes, Bricolage Grotesque carries the wordmark, titles and the score, and Movie A / Movie B wear the blue and magenta of three-strip Technicolor on their posters instead of colouring whole rows. The loop closes on screen - connectors light, a gold line draws back to Movie A, the row glows, then the result arrives - in place of confetti, on every play mode, scaled to the chain's length. Every page shares one header and hairline lists; copy is sentence case with one verb per flow, and the Home hero shows a real finished loop. Also clamps link counts at zero: an opponent who had not moved yet showed "-1 links".
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
import { useGameStore } from '@/stores/game-store';
|
|
import { Loader2, AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
export default function ValidationFeedback() {
|
|
const isValidating = useGameStore((s) => s.isValidating);
|
|
const validationError = useGameStore((s) => s.validationError);
|
|
const chain = useGameStore((s) => s.chain);
|
|
const [showCheck, setShowCheck] = useState(false);
|
|
const [flashedLength, setFlashedLength] = useState(0);
|
|
|
|
// Flash a gold check when the chain grows (successful validation). Adjusting
|
|
// state during render is React's documented pattern for reacting to a changed
|
|
// value; doing it in an effect costs an extra committed render each link.
|
|
if (chain.length > 1 && chain.length !== flashedLength) {
|
|
setFlashedLength(chain.length);
|
|
setShowCheck(true);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!showCheck) return;
|
|
const timer = setTimeout(() => setShowCheck(false), 600);
|
|
return () => clearTimeout(timer);
|
|
}, [showCheck]);
|
|
|
|
if (isValidating) {
|
|
return (
|
|
<div
|
|
role="status"
|
|
className="flex items-center gap-2 text-sm text-muted-foreground"
|
|
>
|
|
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
|
|
Checking the link…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (validationError) {
|
|
return (
|
|
<div
|
|
key={validationError}
|
|
role="alert"
|
|
className="flex animate-shake items-center gap-2 text-sm text-destructive motion-reduce:animate-none"
|
|
>
|
|
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
{validationError}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (showCheck) {
|
|
return (
|
|
<div
|
|
role="status"
|
|
className="flex animate-check-flash items-center gap-2 text-sm font-medium text-primary-text motion-reduce:animate-none"
|
|
>
|
|
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
|
|
Linked
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|