Review breakdown: show each deck's own activity total

Follow-up to the smart-review breakdown: "I only have 2 decks" turned out to
be three deck cards (one clipped at the window edge) and 15 + 13 + 6 = 34
due. The line now carries each deck's activity total next to its due count -
"Due by deck: Sky deck - 15 of 15 - Humanismus - 13 of 15 - ... - 6 of 13" -
so the arithmetic is checkable inline without opening a deck.

groupByDeck returns {label, count, total}; new i18n key review.dueOfTotal
(en + cs). review-test pins the format. Version 0.6.9, SW v91.
This commit is contained in:
Martin Dvořák 2026-10-15 21:20:53 +00:00
commit 71c56ad06f
7 changed files with 24 additions and 8 deletions

View file

@ -1492,6 +1492,14 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged. scene-heat rework unchanged.
100. **The review breakdown carries each deck's own total.** The follow-up to 99: "bro its not
possible, I only have 2 decks" — the screenshot showed three deck cards (one clipped at the
window edge) and a breakdown of 15 + 13 + 6 = 34, so the count was right and the *number of
decks* was the misread. To make that checkable inline, each entry now carries the deck's
activity total next to its due count — "Due by deck: Renesanční… · 15 of 15 · Humanismus… ·
13 of 15 · Renesance… · 6 of 13" — so the arithmetic (34 due out of 43 activities) is
verifiable without opening a single deck. review-test pins the format.
99. **The smart review names its decks.** "It contains mistakes I either did more than once or 99. **The smart review names its decks.** "It contains mistakes I either did more than once or
from decks that I've removed — there's no way there's 34 to repeat with 2 decks." The count from decks that I've removed — there's no way there's 34 to repeat with 2 decks." The count
itself was honest: `dueItems`/`mistakeItems` only resolve live (deck, activity) pairs, so a itself was honest: `dueItems`/`mistakeItems` only resolve live (deck, activity) pairs, so a

View file

@ -680,13 +680,17 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
); );
} }
/** "Sky deck · 17 · Biology · 14 (+2 more)" — capped so the line stays short. */ /** "Sky deck · 17 of 17 · Biology · 14 of 15 (+2 more)" — the deck's own
* activity total rides along, so a count that mixes decks can be checked
* against the decks instead of looking impossible. Capped at three decks. */
function deckBreakdown( function deckBreakdown(
items: ReviewItem[], items: ReviewItem[],
t: (key: string, vars?: Record<string, string | number>) => string, t: (key: string, vars?: Record<string, string | number>) => string,
): string { ): string {
const groups = groupByDeck(items); const groups = groupByDeck(items);
const shown = groups.slice(0, 3).map((group) => `${group.label} · ${group.count}`); const shown = groups
.slice(0, 3)
.map((group) => `${group.label} · ${t("review.dueOfTotal", { due: group.count, total: group.total })}`);
const rest = groups.length - shown.length; const rest = groups.length - shown.length;
if (rest > 0) shown.push(t("review.moreDecks", { n: rest })); if (rest > 0) shown.push(t("review.moreDecks", { n: rest }));
return shown.join(" · "); return shown.join(" · ");

View file

@ -846,6 +846,7 @@ const en: Record<string, string> = {
"review.due.few": "{n} due", "review.due.few": "{n} due",
"review.due.many": "{n} due", "review.due.many": "{n} due",
"review.dueByDeck": "Due by deck: {list}", "review.dueByDeck": "Due by deck: {list}",
"review.dueOfTotal": "{due} of {total}",
"review.mistakesByDeck": "Mistakes by deck: {list}", "review.mistakesByDeck": "Mistakes by deck: {list}",
"review.moreDecks.one": "+{n} more deck", "review.moreDecks.one": "+{n} more deck",
"review.moreDecks.few": "+{n} more decks", "review.moreDecks.few": "+{n} more decks",
@ -1852,6 +1853,7 @@ const cs: Record<string, string> = {
"review.due.few": "{n} k opakování", "review.due.few": "{n} k opakování",
"review.due.many": "{n} k opakování", "review.due.many": "{n} k opakování",
"review.dueByDeck": "K opakování podle sady: {list}", "review.dueByDeck": "K opakování podle sady: {list}",
"review.dueOfTotal": "{due} z {total}",
"review.mistakesByDeck": "Chyby podle sady: {list}", "review.mistakesByDeck": "Chyby podle sady: {list}",
"review.moreDecks.one": "+{n} další sada", "review.moreDecks.one": "+{n} další sada",
"review.moreDecks.few": "+{n} další sady", "review.moreDecks.few": "+{n} další sady",

View file

@ -77,11 +77,13 @@ export function mistakeCount(answers: AnswerRecord[], deckId?: string): number {
* ("34 due" with two decks on screen) is self-explanatory instead of looking * ("34 due" with two decks on screen) is self-explanatory instead of looking
* like the same mistake twice or an item from a deck that was deleted. * like the same mistake twice or an item from a deck that was deleted.
*/ */
export function groupByDeck(items: ReviewItem[]): { label: string; count: number }[] { /** `total` is the deck's activity count, so "34 due" can be checked against
const groups = new Map<string, { label: string; count: number }>(); * the decks themselves ("15 of 15 · 13 of 15 · 6 of 13"). */
export function groupByDeck(items: ReviewItem[]): { label: string; count: number; total: number }[] {
const groups = new Map<string, { label: string; count: number; total: number }>();
for (const item of items) { for (const item of items) {
const key = item.deck.id; const key = item.deck.id;
const group = groups.get(key) ?? { label: item.deck.title, count: 0 }; const group = groups.get(key) ?? { label: item.deck.title, count: 0, total: item.deck.activities.length };
group.count += 1; group.count += 1;
groups.set(key, group); groups.set(key, group);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "freezmo", "name": "freezmo",
"version": "0.6.8", "version": "0.6.9",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {

View file

@ -9,7 +9,7 @@
* *
* Bumping CACHE_VERSION is how a new deploy invalidates old assets. * Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/ */
const CACHE_VERSION = "freezmo-v90"; const CACHE_VERSION = "freezmo-v91";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`; const ASSET_CACHE = `${CACHE_VERSION}-assets`;

View file

@ -189,7 +189,7 @@ async function makeReviewPage(locale = "en") {
); );
check( check(
"review: the card breaks the count down by deck", "review: the card breaks the count down by deck",
breakdown === "Due by deck: Sky deck · 2 · Twin deck · 2", breakdown === "Due by deck: Sky deck · 2 of 2 · Twin deck · 2 of 2",
breakdown, breakdown,
); );