First implementation of 10 finger typing
This commit is contained in:
248
tippen/src/components/ResultSheet.tsx
Normal file
248
tippen/src/components/ResultSheet.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
/** What a run earned.
|
||||
*
|
||||
* The rule this screen exists to enforce: there is no losing screen. A bad run shows
|
||||
* fewer stars and a slower animal, and the primary button still says "Nochmal" with the
|
||||
* focus already on it. Nothing here ever says "failed", nothing is red, and the Perlen
|
||||
* always go up - see `perlenFor` in lib/grading.ts. */
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { kreaturById } from "../lib/aquarium";
|
||||
import type { KreaturId } from "../lib/aquarium";
|
||||
import type { RunResult } from "../lib/grading";
|
||||
import { STERNE_SCHWELLEN, sichtbareTiers, tierById, tierIndex, tierProgress } from "../lib/grading";
|
||||
import type { TierId } from "../lib/grading";
|
||||
|
||||
interface Props {
|
||||
result: RunResult;
|
||||
/** Set when this run opened the next lesson. */
|
||||
unlockedTitel: string | null;
|
||||
/** Set when this run released a creature into the aquarium. */
|
||||
neuesTier: KreaturId | null;
|
||||
bestseit: boolean;
|
||||
/** The fastest animal earned on any lesson so far - decides how much of the ladder
|
||||
* may be revealed. */
|
||||
bestEver: TierId | null;
|
||||
onNochmal: () => void;
|
||||
onWeiter: () => void;
|
||||
/** Null at the end of the curriculum. */
|
||||
weiterLabel: string | null;
|
||||
}
|
||||
|
||||
export function ResultSheet({
|
||||
result,
|
||||
unlockedTitel,
|
||||
neuesTier,
|
||||
bestseit,
|
||||
bestEver,
|
||||
onNochmal,
|
||||
onWeiter,
|
||||
weiterLabel,
|
||||
}: Props) {
|
||||
const tier = tierById(result.tier);
|
||||
const leiter = sichtbareTiers(result.tier, bestEver);
|
||||
const erreicht = tierIndex(result.tier);
|
||||
const nochmal = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Enter repeats the run. Fewest keystrokes between "that was fun" and "again".
|
||||
useEffect(() => nochmal.current?.focus(), []);
|
||||
|
||||
return (
|
||||
<div className="overlay backdrop-enter">
|
||||
<div
|
||||
className="sheet sheet-enter"
|
||||
style={{ padding: "30px 38px 28px", width: "min(520px, 100%)", textAlign: "center" }}
|
||||
>
|
||||
<div style={{ fontSize: 88, lineHeight: 1, animation: "tierEnter 520ms ease-out" }}>
|
||||
{tier.emoji}
|
||||
</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 900, color: "var(--ink)", marginTop: 6 }}>
|
||||
{tier.name}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "center", gap: 8, margin: "14px 0 4px" }}>
|
||||
{[1, 2, 3].map((stern) => (
|
||||
<span
|
||||
key={stern}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
animation: `sterneEnter 320ms ease-out ${140 + stern * 130}ms both`,
|
||||
filter: result.sterne >= stern ? "none" : "grayscale(1)",
|
||||
opacity: result.sterne >= stern ? 1 : 0.25,
|
||||
}}
|
||||
>
|
||||
⭐
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "center", gap: 26, marginTop: 12 }}>
|
||||
<Stat label="Richtig" value={`${Math.round(result.genauigkeit * 100)}%`} />
|
||||
<Stat label="Zeichen/Min" value={String(Math.round(result.tempo))} />
|
||||
<Stat label="Perlen" value={`+${result.perlen}`} />
|
||||
</div>
|
||||
|
||||
{/* How close the next animal is. A near miss is the strongest reason to press
|
||||
Nochmal, so it is worth showing explicitly. */}
|
||||
<div
|
||||
style={{
|
||||
height: 9,
|
||||
borderRadius: 999,
|
||||
background: "oklch(88% 0.02 175)",
|
||||
margin: "18px 0 6px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${tierProgress(result.punkte) * 100}%`,
|
||||
height: "100%",
|
||||
background: "var(--accent)",
|
||||
transition: "width 700ms ease-out",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TierLeiter tiers={leiter.tiers} erreicht={erreicht} mehrVerborgen={leiter.mehrVerborgen} />
|
||||
|
||||
{bestseit && (
|
||||
<div style={{ color: "var(--accent)", fontWeight: 900, marginTop: 8 }}>
|
||||
🏅 Neuer Bestwert!
|
||||
</div>
|
||||
)}
|
||||
{unlockedTitel && (
|
||||
<div style={{ color: "var(--ink)", fontWeight: 900, marginTop: 8 }}>
|
||||
🔓 Neu: {unlockedTitel}
|
||||
</div>
|
||||
)}
|
||||
{neuesTier && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 10,
|
||||
color: "var(--ink)",
|
||||
fontWeight: 900,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={kreaturById(neuesTier).bild}
|
||||
alt=""
|
||||
style={{ width: 52, height: 52, objectFit: "contain", animation: "tierEnter 520ms ease-out" }}
|
||||
/>
|
||||
{kreaturById(neuesTier).artikel} {kreaturById(neuesTier).name} ist ins Aquarium gezogen!
|
||||
</div>
|
||||
)}
|
||||
{!result.bestanden && !unlockedTitel && (
|
||||
<div style={{ color: "var(--ink)", opacity: 0.75, fontWeight: 700, marginTop: 10 }}>
|
||||
Mit {Math.round(STERNE_SCHWELLEN.zwei * 100)}% Treffern geht es weiter. Fast!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", gap: 12, marginTop: 22, justifyContent: "center" }}>
|
||||
<button
|
||||
ref={nochmal}
|
||||
onClick={onNochmal}
|
||||
style={{
|
||||
...knopf,
|
||||
background: "var(--accent)",
|
||||
color: "var(--paper)",
|
||||
}}
|
||||
>
|
||||
Nochmal ⏎
|
||||
</button>
|
||||
{weiterLabel && (
|
||||
<button onClick={onWeiter} style={{ ...knopf, background: "oklch(90% 0.02 175)", color: "var(--ink)" }}>
|
||||
{weiterLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** The animal ladder.
|
||||
*
|
||||
* Everything she has already passed stays in full colour - the run is a climb, and the
|
||||
* rungs below are what shows how far she has come. The rungs above are greyed out but
|
||||
* still legible, because the next animal has to be visible to be worth aiming at.
|
||||
*
|
||||
* Above the Delfin the ladder stops: those animals are not shown at all until they are
|
||||
* reached. A single "?" says the ladder continues without saying what is on it, which
|
||||
* keeps the top end a surprise rather than a distant, discouraging number. */
|
||||
function TierLeiter({
|
||||
tiers,
|
||||
erreicht,
|
||||
mehrVerborgen,
|
||||
}: {
|
||||
tiers: readonly { id: string; name: string; emoji: string }[];
|
||||
erreicht: number;
|
||||
mehrVerborgen: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-end",
|
||||
gap: 4,
|
||||
flexWrap: "wrap",
|
||||
marginTop: 14,
|
||||
minHeight: 44,
|
||||
}}
|
||||
aria-label="Tier-Leiter"
|
||||
>
|
||||
{tiers.map((eintrag, i) => {
|
||||
const geschafft = i <= erreicht;
|
||||
const aktuell = i === erreicht;
|
||||
return (
|
||||
<div
|
||||
key={eintrag.id}
|
||||
title={eintrag.name}
|
||||
style={{
|
||||
fontSize: aktuell ? 40 : 26,
|
||||
lineHeight: 1,
|
||||
padding: aktuell ? "0 5px" : 0,
|
||||
filter: geschafft ? "none" : "grayscale(1)",
|
||||
// Bright enough to read as "this is next", dim enough to read as "not yet".
|
||||
opacity: geschafft ? 1 : 0.42,
|
||||
transform: aktuell ? "translateY(-3px)" : undefined,
|
||||
transition: "all 200ms ease",
|
||||
}}
|
||||
>
|
||||
{eintrag.emoji}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{mehrVerborgen && (
|
||||
<div
|
||||
title="Da geht noch was!"
|
||||
style={{ fontSize: 24, opacity: 0.4, marginLeft: 4, filter: "grayscale(1)" }}
|
||||
>
|
||||
❓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const knopf: React.CSSProperties = {
|
||||
border: "none",
|
||||
borderRadius: 999,
|
||||
padding: "13px 26px",
|
||||
fontSize: 17,
|
||||
fontWeight: 900,
|
||||
cursor: "pointer",
|
||||
};
|
||||
|
||||
function Stat({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: 25, fontWeight: 900, color: "var(--ink)" }}>{value}</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: "var(--ink)", opacity: 0.6 }}>{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user