1
0
Fork 0

feat(minigames): Esc/R reset keybind with hint text for OPORD Recall

This commit is contained in:
Jason Fraley 2026-09-08 12:13:45 -04:00
parent 494b60db3b
commit e52a637f96

View file

@ -265,6 +265,18 @@ function OpordBriefingCanvas({
);
}
function ResetHint({ className }: { className?: string }) {
const keyClass =
"rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px] leading-none";
return (
<p className={cn("text-xs text-muted-foreground", className)}>
Press{" "}
<kbd className={keyClass}>Esc</kbd> or <kbd className={keyClass}>R</kbd> at any point to
reset the exercise.
</p>
);
}
function Metre({ label, value }: { label: string; value: string | number }) {
return (
<div className="rounded-lg border border-border bg-background px-4 py-3 text-center">
@ -353,6 +365,9 @@ export function OpordGame({
submittingRef.current = false;
setSubmitting(false);
// Esc/R can reset the run while the submission is in flight; global stats
// stay in sync, but the run-result UI is skipped for a cancelled run.
const cancelled = phaseRef.current !== "quiz";
if (res.success && res.data) {
statsRef.current = res.data;
onGameRecordedAction(res.data);
@ -364,14 +379,16 @@ export function OpordGame({
);
window.setTimeout(() => router.refresh(), 1700);
}
setLastRun({
correct,
total: preview.total,
score: res.data.score,
xpEarned: res.data.xpEarned,
best: res.data.bestScore,
});
} else {
if (!cancelled) {
setLastRun({
correct,
total: preview.total,
score: res.data.score,
xpEarned: res.data.xpEarned,
best: res.data.bestScore,
});
}
} else if (!cancelled) {
setLastRun({
correct,
total: preview.total,
@ -381,7 +398,7 @@ export function OpordGame({
error: res.error ?? "Could not record score.",
});
}
setPhaseSafe("over");
if (!cancelled) setPhaseSafe("over");
}, [difficulty, expiredRef, onGameRecordedAction, router, setPhaseSafe]);
const startBriefing = useCallback(() => {
@ -411,6 +428,36 @@ export function OpordGame({
setPhaseSafe("quiz");
}, [setPhaseSafe]);
const resetGame = useCallback(() => {
scenarioSeedRef.current = 0;
setScenario(null);
setQuestions([]);
setQuestionIndex(0);
setTypedAnswer("");
answersRef.current = [];
lastAnswerAtRef.current = 0;
setLastAnswerWasCorrect(null);
setLastRun(null);
setQuizRemainingMs(tier.quizSeconds * 1000);
setPhaseSafe("ready");
}, [setPhaseSafe, tier.quizSeconds]);
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return;
const target = event.target;
const typing =
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement ||
(target instanceof HTMLElement && target.isContentEditable);
const key = event.key.toLowerCase();
if (key === "escape" || (key === "r" && !typing)) resetGame();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [resetGame]);
const answerCurrent = useCallback(
(value: string) => {
if (phaseRef.current !== "quiz") return;
@ -484,6 +531,7 @@ export function OpordGame({
<Button type="button" size="lg" onClick={startBriefing} disabled={expired}>
Receive briefing
</Button>
<ResetHint />
</div>
)}
@ -606,6 +654,8 @@ export function OpordGame({
Your session is about to end. The app will refresh when it expires.
</p>
)}
{phase !== "ready" && <ResetHint className="text-center" />}
</div>
);
}