Separate Study Decks and Homework in the library; drop the /create mode switch

Library ("Your icebox")
- Two tabs, never mixed: Study Decks (the folder + card library) and Homework
  (saved solutions and step-by-step guides). The active tab lives in the URL
  (/?tab=homework), so the sections are linkable and survive a reload.
- Decks tab keeps Import, the folder bar, stats and its own empty state — now
  a single decks-only CTA.
- Homework tab lists saved results as cards (mode icon, date, step count,
  preview) with Open and delete-with-confirm; its empty state has its own CTA
  ("Homework Solver") that opens the solver directly. Opening a card
  deep-links to /homework?open=<id>, which the screen opens on arrival.
- .segmented now also styles link segments (same control as settings).

/create
- The redundant "Study Deck | Homework Solver" toggle is gone: /create is the
  deck uploader only, and the bottom bar's + goes straight to it. Homework has
  its own screen, reached from the library tab, the drawer and CTAs.

Copy
- New keys (en + cs): tabDecks, tabHomework, homeworkHeroTitle, plus homework
  empty-state/open/step-count strings. The homework hero is now "Solve your
  homework" / "Vyřeš svůj úkol" — no study-deck text in the homework flow.
- Removed the now-unused ModeSwitch component and homework.title key.

Service worker: freezmo-v10.

Verified: library-tabs suite 37/37 (tab switching, context empty states,
deep-link opening, deletion, no switch on /create, heroes, redirects, desktop
grid, 360px + 44px targets, axe contrast en+cs light+dark), mobile 49/49,
i18n 27/27, desktop regression 74/74. Lint + build clean.
This commit is contained in:
Martin Dvořák 2026-10-08 21:26:08 +00:00
commit 8c44f47881
12 changed files with 295 additions and 162 deletions

View file

@ -89,8 +89,9 @@ no database, no data leaving your machine except the requests you send to Ollama
why, **three progressive hints** per step (nudge → hint → the full answer), and a **checkpoint**
built from the normal activity components and graded through `/api/grade`
- The last screen assembles the solution from the step answers, with a copy button
- Results live in `freezmo.homework.v1` with a small history you can reopen or delete; **Make it
again** re-runs from the stored assignment text — original files are never saved
- Results live in `freezmo.homework.v1` — the homework screen's history and the library's
**Homework** tab — where you can reopen or delete them; **Make it again** re-runs from the
stored assignment text, and original files are never saved
- The answer and the guide are always written in the **language of the assignment**, never
translated into the interface language
@ -116,6 +117,9 @@ no database, no data leaving your machine except the requests you send to Ollama
**Library & folders**
- **Two pools, never mixed**: a **Study Decks** tab (the folder + card library) and a **Homework**
tab with saved solutions and step-by-step guides — each with its own empty state and call to
action, and the active tab lives in the URL (`/?tab=homework`)
- Decks saved locally with title, subject, folder, activity count, date, play count and best score
- **Folders**: create named groups ("History", "Biology", …) from the sidebar or the create screen.
New decks freeze straight into the selected folder; anything unfiled lives in **Unorganized**.

View file

@ -492,6 +492,28 @@ body {
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
}
/* Link segments (library tabs): same control, but links navigate. */
.segmented > a {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
border-radius: 0.75rem;
padding: 0.42rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-frost-700);
transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.dark .segmented > a {
color: var(--color-frost-300);
}
.segmented > a[aria-current="page"] {
background-image: linear-gradient(135deg, #38bdf8 0%, #60a5fa 100%);
color: #06111f;
box-shadow: 0 6px 16px -8px rgb(56 189 248 / 0.7);
}
/* Ice gradient for progress bars and selected states. */
.grad-ice {
@ -705,7 +727,8 @@ body {
@media (max-width: 639px) {
.touch-target { min-height: 44px; min-width: 44px; }
.nav-row { min-height: 44px; }
.segmented > button { min-height: 44px; }
.segmented > button,
.segmented > a { min-height: 44px; }
.folder-pill { min-height: 44px; }
}

View file

@ -15,7 +15,7 @@ export default async function HomeworkPage({
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { mode } = await searchParams;
const { mode, open } = await searchParams;
// The mode switch and the library quick actions deep-link into a mode; a
// link that points at the Study Deck side is handed back to /create.
if (mode === "deck") redirect("/create?mode=deck");
@ -29,7 +29,7 @@ export default async function HomeworkPage({
<main className="flex min-h-dvh min-w-0 flex-1 flex-col">
<AppHeader />
<div className="mx-auto w-full max-w-3xl flex-1 px-4 pb-tabbar lg:px-10 lg:pb-16 lg:pt-10">
<HomeworkScreen />
<HomeworkScreen initialRecordId={typeof open === "string" ? open : undefined} />
</div>
<TabBar />
</main>

View file

@ -4,7 +4,12 @@ import { LibraryScreen } from "@/components/LibraryScreen";
import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
import { TabBar } from "@/components/TabBar";
export default function HomePage() {
export default async function HomePage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { tab } = await searchParams;
return (
<ApiKeyGate>
<div className="flex min-h-dvh">
@ -14,7 +19,7 @@ export default function HomePage() {
<main className="flex min-h-dvh min-w-0 flex-1 flex-col">
<AppHeader />
<div className="mx-auto w-full max-w-6xl flex-1 px-4 pb-tabbar lg:px-10 lg:pb-16 lg:pt-10">
<LibraryScreen />
<LibraryScreen initialTab={tab === "homework" ? "homework" : "decks"} />
</div>
<TabBar />
</main>

View file

@ -0,0 +1,97 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { BoltIcon, PlayIcon, XIcon } from "@/components/icons";
import { Button } from "@/components/ui";
import { useI18n } from "@/lib/i18n";
import type { HomeworkRecord } from "@/lib/schemas";
import { formatDate, truncate } from "@/lib/utils";
/**
* One saved homework result in the library's Homework tab: the finished
* solution or the step-by-step guide, with open + delete.
*/
export function HomeworkCard({ record, onDelete }: { record: HomeworkRecord; onDelete: (id: string) => void }) {
const { t, plural } = useI18n();
const [confirmDelete, setConfirmDelete] = useState(false);
const href = `/homework?open=${record.id}`;
const preview =
record.mode === "make"
? truncate(record.answer.replace(/\s+/g, " ").trim(), 150)
: truncate(
record.steps.map((step) => step.title).join(" · "),
150,
);
return (
<li className="card card-hover group relative flex flex-col p-5">
{/* Desktop: the whole card opens the result; the delete button sits above it. */}
<Link href={href} aria-hidden="true" tabIndex={-1} className="absolute inset-0 hidden rounded-[1.5rem] lg:block" />
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<h3
className="line-clamp-2 text-[15px] font-semibold leading-snug tracking-[-0.01em] text-[var(--text)]"
title={record.title}
>
{truncate(record.title, 90)}
</h3>
<p className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[12.5px] text-[var(--text-subtle)]">
<span className="inline-flex items-center gap-1">
{record.mode === "make" ? <BoltIcon className="size-3.5" /> : <PlayIcon className="size-3.5" />}
{t(record.mode === "make" ? "homework.history.made" : "homework.history.guide")}
</span>
<span aria-hidden>·</span>
{formatDate(record.updatedAt)}
{record.mode === "guide" ? (
<>
<span aria-hidden>·</span>
{plural(record.steps.length, "homework.steps")}
</>
) : null}
</p>
</div>
<button
type="button"
aria-label={`${t("common.delete")} — ${record.title}`}
onClick={() => setConfirmDelete(true)}
className="touch-target relative z-10 grid size-8 shrink-0 place-items-center rounded-full text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-rose-600 dark:hover:text-rose-300"
>
<XIcon className="size-4" />
</button>
</div>
{preview ? (
<p className="mt-3 line-clamp-2 text-[13.5px] leading-relaxed text-[var(--text-muted)]">{preview}</p>
) : null}
<div className="mt-5 lg:mt-4">
{confirmDelete ? (
<div className="relative z-10 flex flex-wrap items-center gap-2">
<span className="text-[12.5px] text-[var(--text-muted)]">{t("homework.history.deleteConfirm")}</span>
<Button size="sm" variant="danger" onClick={() => onDelete(record.id)}>
{t("common.delete")}
</Button>
<Button size="sm" variant="ghost" onClick={() => setConfirmDelete(false)}>
{t("common.cancel")}
</Button>
</div>
) : (
<>
<Link href={href} className="btn-primary touch-target h-10 w-full px-4 text-sm lg:hidden">
{t("homework.open")}
</Link>
<div className="relative z-10 hidden lg:flex lg:justify-end">
<Link href={href} className="btn-secondary h-9 px-4 text-[13px]">
{t("homework.open")}
</Link>
</div>
</>
)}
</div>
</li>
);
}

View file

@ -5,9 +5,11 @@ import { useMemo, useRef, useState, useSyncExternalStore } from "react";
import { DeckCard } from "@/components/DeckCard";
import { FolderBar } from "@/components/FolderBar";
import { HomeworkCard } from "@/components/HomeworkCard";
import { FolderIcon, SnowflakeIcon, SparklesIcon, UploadIcon } from "@/components/icons";
import { Button, EmptyState } from "@/components/ui";
import { folderStore, renameFolder } from "@/lib/folders";
import { deleteHomework, homeworkStore, sortHomework } from "@/lib/homework";
import { useI18n } from "@/lib/i18n";
import { ALL_FOLDERS, folderSelection, useSelectedFolder } from "@/lib/nav";
import { openSettings } from "@/lib/ui-events";
@ -16,11 +18,19 @@ import { deckStore, deleteFolder, importDeck, sortDecks, upsertDeck } from "@/li
import { cn } from "@/lib/utils";
/**
* Dashboard: page title, folder pills, then a spacious responsive grid of
* deck cards grouped by folder.
* Dashboard with two separate pools: study decks (folders + card grid) and
* saved homework (solutions and step-by-step guides). The tab bar picks one,
* so the two never mix in a single list.
*/
export function LibraryScreen() {
export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" | "homework" }) {
const tab = initialTab;
const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot);
const homeworkRecords = useSyncExternalStore(
homeworkStore.subscribe,
homeworkStore.getSnapshot,
homeworkStore.getServerSnapshot,
);
const homework = useMemo(() => sortHomework(homeworkRecords), [homeworkRecords]);
const folders = useSyncExternalStore(
folderStore.subscribe,
folderStore.getSnapshot,
@ -119,38 +129,57 @@ export function LibraryScreen() {
</div>
<div className="flex flex-wrap items-center gap-2">
{/* With nothing in the library, Import drops to a quiet text link
so the single primary action is “Make your first deck”. */}
{isEmpty ? (
<button
type="button"
onClick={() => importRef.current?.click()}
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline"
>
<UploadIcon className="size-4" />
{t("library.import")}
</button>
) : (
<>
<Button
variant="secondary"
size="md"
className="touch-target lg:hidden"
onClick={() => importRef.current?.click()}
>
<UploadIcon className="size-4" />
{t("library.import")}
</Button>
{/* Context action: Import for decks (a quiet link while the pool
is empty), start-a-new-solve for homework. */}
{tab === "decks" ? (
isEmpty ? (
<button
type="button"
onClick={() => importRef.current?.click()}
className="hidden items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline lg:inline-flex"
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline"
>
<UploadIcon className="size-4" />
{t("library.import")}
</button>
) : (
<>
<Button
variant="secondary"
size="md"
className="touch-target lg:hidden"
onClick={() => importRef.current?.click()}
>
<UploadIcon className="size-4" />
{t("library.import")}
</Button>
<button
type="button"
onClick={() => importRef.current?.click()}
className="hidden items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline lg:inline-flex"
>
<UploadIcon className="size-4" />
{t("library.import")}
</button>
</>
)
) : homework.length > 0 ? (
<>
<Link
href="/homework?mode=homework"
className="btn-secondary touch-target h-10 px-4 text-sm lg:hidden"
>
<SparklesIcon className="size-4" />
{t("actionHomework")}
</Link>
<Link
href="/homework?mode=homework"
className="hidden items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline lg:inline-flex"
>
<SparklesIcon className="size-4" />
{t("actionHomework")}
</Link>
</>
)}
) : null}
<input
ref={importRef}
type="file"
@ -162,11 +191,21 @@ export function LibraryScreen() {
}}
/>
</div>
{/* Two pools, never mixed: study decks and saved homework. */}
<div className="segmented w-full sm:max-w-sm">
<Link href="/" aria-current={tab === "decks" ? "page" : undefined}>
{t("tabDecks")}
</Link>
<Link href="/?tab=homework" aria-current={tab === "homework" ? "page" : undefined}>
{t("tabHomework")}
</Link>
</div>
</div>
{/* Secondary stats folded into the header as quiet text on desktop,
where the sidebar already shows per-folder counts. */}
{decks.length > 0 ? (
{tab === "decks" && decks.length > 0 ? (
<p className="text-[13px] text-[var(--text-subtle)] lg:hidden">
{plural(decks.length, "stats.decks")}
<span aria-hidden> · </span>
@ -185,7 +224,7 @@ export function LibraryScreen() {
{/* Mobile folder switcher; the sidebar covers desktop. With an empty
library there is nothing to filter, and the pills would compete
with the empty state's single call to action. */}
{!isEmpty ? (
{tab === "decks" && !isEmpty ? (
<FolderBar
className="lg:hidden"
folders={folderList}
@ -206,7 +245,7 @@ export function LibraryScreen() {
) : null}
{message ? (
{tab === "decks" && message ? (
<p
role="status"
className={cn(
@ -219,43 +258,25 @@ export function LibraryScreen() {
) : null}
</section>
{/* deck grid ------------------------------------------------------ */}
{isEmpty ? (
{/* decks: folders + card grid, or a decks-only empty state --------- */}
{tab === "decks" && isEmpty ? (
<EmptyState
title={t("library.noDecks")}
action={
/* Two ways in: build a study deck from notes, or hand over an
assignment. Stacked on phones, side by side from 640px up. */
<div className="grid w-[min(24rem,calc(100vw_-_3rem))] gap-3 sm:w-[36rem] sm:grid-cols-2">
<Link
href="/create?mode=deck"
className="card card-hover touch-target flex items-center gap-3 border-accent-400/60 p-3.5 text-left sm:p-4"
<Link
href="/create?mode=deck"
className="card card-hover touch-target flex w-[min(24rem,calc(100vw_-_3rem))] items-center gap-3 border-accent-400/60 p-3.5 text-left sm:p-4"
>
<span
aria-hidden
className="grad-ice grid size-11 shrink-0 place-items-center rounded-2xl text-[#06111f]"
>
<span
aria-hidden
className="grad-ice grid size-11 shrink-0 place-items-center rounded-2xl text-[#06111f]"
>
<SnowflakeIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateCreateDeck")}
</span>
</Link>
<Link
href="/homework?mode=homework"
className="card card-hover touch-target flex items-center gap-3 p-3.5 text-left sm:p-4"
>
<span
aria-hidden
className="grid size-11 shrink-0 place-items-center rounded-2xl bg-[var(--surface-sunken)] text-[var(--text-muted)]"
>
<SparklesIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateHomework")}
</span>
</Link>
</div>
<SnowflakeIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateCreateDeck")}
</span>
</Link>
}
>
{t("library.emptyBody")}
@ -271,7 +292,10 @@ export function LibraryScreen() {
.
</span>
</EmptyState>
) : (
) : null}
{/* deck grid (the decks tab, once there is something to show) ------- */}
{tab === "decks" && !isEmpty ? (
<div className="space-y-8">
{groups.map((group) => {
const isEditing = editingFolder?.id === group.id;
@ -393,7 +417,40 @@ export function LibraryScreen() {
);
})}
</div>
)}
) : null}
{/* homework: saved solutions and guides, or a homework-only empty state */}
{tab === "homework" && homework.length === 0 ? (
<EmptyState
title={t("homework.emptyTitle")}
action={
<Link
href="/homework?mode=homework"
className="card card-hover touch-target flex w-[min(24rem,calc(100vw_-_3rem))] items-center gap-3 border-accent-400/60 p-3.5 text-left sm:p-4"
>
<span
aria-hidden
className="grad-ice grid size-11 shrink-0 place-items-center rounded-2xl text-[#06111f]"
>
<SparklesIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateHomework")}
</span>
</Link>
}
>
{t("homework.emptyBody")}
</EmptyState>
) : null}
{tab === "homework" && homework.length > 0 ? (
<ul className="grid list-none grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
{homework.map((record) => (
<HomeworkCard key={record.id} record={record} onDelete={deleteHomework} />
))}
</ul>
) : null}
</div>

View file

@ -1,80 +0,0 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { SnowflakeIcon, SparklesIcon } from "@/components/icons";
import { useI18n } from "@/lib/i18n";
import { cn } from "@/lib/utils";
export type StudyMode = "deck" | "homework";
/** Canonical URL per mode, so a deep link always lands on the right screen. */
export const MODE_HREF: Record<StudyMode, string> = {
deck: "/create?mode=deck",
homework: "/homework?mode=homework",
};
/** How long the ice pill takes to slide before the route follows. */
const SLIDE_MS = 180;
/**
* Compact two-segment mode switch (Study Deck / Homework Solver). The ice pill
* slides to the chosen segment and the router follows a moment later, so the
* toggle reads as one smooth transition instead of a hard jump. The current
* mode stays pressed; the other segment navigates.
*/
export function ModeSwitch({ current }: { current: StudyMode }) {
const { t } = useI18n();
const router = useRouter();
const [pending, setPending] = useState<StudyMode | null>(null);
const timer = useRef<number | null>(null);
useEffect(
() => () => {
if (timer.current !== null) window.clearTimeout(timer.current);
},
[],
);
const choose = (mode: StudyMode) => {
if (mode === current || pending !== null) return;
setPending(mode);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
timer.current = window.setTimeout(() => router.push(MODE_HREF[mode]), reduce ? 0 : SLIDE_MS + 10);
};
const shown = pending ?? current;
const segment = (mode: StudyMode, icon: React.ReactNode, label: string) => (
<button
type="button"
aria-pressed={current === mode}
onClick={() => choose(mode)}
className={cn(
"relative z-10 inline-flex min-h-11 items-center justify-center gap-1.5 rounded-xl px-2 text-[13.5px] font-semibold transition-colors",
shown === mode ? "text-[#06111f]" : "text-frost-700 hover:text-[var(--text)] dark:text-frost-300",
)}
>
{icon}
<span className="whitespace-nowrap">{label}</span>
</button>
);
return (
<div
data-testid="mode-switch"
className="relative grid grid-cols-2 rounded-[0.95rem] border border-[var(--hairline)] bg-[var(--surface-sunken)] p-[3px] backdrop-blur-sm"
>
<span
aria-hidden
className={cn(
"grad-ice pointer-events-none absolute inset-y-[3px] left-[3px] w-[calc(50%-3px)] rounded-xl shadow-[0_6px_16px_-8px_rgb(56_189_248/0.7)] transition-transform duration-[180ms] ease-out motion-reduce:transition-none",
shown === "homework" && "translate-x-full",
)}
/>
{segment("deck", <SnowflakeIcon className="size-4" />, t("actionStudyDeck"))}
{segment("homework", <SparklesIcon className="size-4" />, t("actionHomework"))}
</div>
);
}

View file

@ -33,7 +33,6 @@ import { upsertDeck } from "@/lib/storage";
import { openSettings } from "@/lib/ui-events";
import { cn, uid } from "@/lib/utils";
import { CameraIcon, FolderIcon, IceCubeIcon, PlusIcon, SnowflakeIcon, SunIcon } from "@/components/icons";
import { ModeSwitch } from "@/components/ModeSwitch";
import { Button, ProgressBar } from "@/components/ui";
type Phase = "idle" | "preparing" | "loading" | "error";
@ -321,9 +320,9 @@ export function UploadScreen() {
</p>
</section>
{/* Mode switcher: a compact segmented control. This page is the Study
Deck side; the other segment opens the homework flow. */}
<ModeSwitch current="deck" />
{/* Dedicated flow: this page is the Study Deck uploader. Homework has
its own screen (/homework), reached from the library tab, the
drawer or the bottom bar — no mode picker here. */}
{error ? (

View file

@ -104,7 +104,7 @@ export function HomeworkInput({
<section className="space-y-2">
<h1 className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[28px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)] text-balance sm:text-[32px]">
<SparklesIcon className="size-8 shrink-0 text-frost-500 dark:text-frost-200" />
<span>{t("homework.title")}</span>
<span>{t("homeworkHeroTitle")}</span>
</h1>
<p className="max-w-xl text-[15px] text-frost-600 dark:text-frost-300">{t("homework.subtitle")}</p>
</section>

View file

@ -1,11 +1,12 @@
"use client";
import { useCallback, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { documentsToText } from "@/lib/documents";
import { ApiError, generateHomework, type HomeworkProgress } from "@/lib/client-api";
import {
deleteHomework,
getHomework,
homeworkStore,
saveHomework,
sortHomework,
@ -28,7 +29,7 @@ const MAX_TEXT_TO_SEND = 100_000;
type View = { kind: "start" } | { kind: "loading" } | { kind: "record"; record: HomeworkRecord };
/** The /homework screen: give it an assignment, pick a mode, get a result. */
export function HomeworkScreen() {
export function HomeworkScreen({ initialRecordId }: { initialRecordId?: string }) {
const { t } = useI18n();
const records = useSyncExternalStore(
homeworkStore.subscribe,
@ -49,6 +50,13 @@ export function HomeworkScreen() {
const [againLoading, setAgainLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
// Deep link from the library's Homework tab: open the saved result directly.
useEffect(() => {
if (!initialRecordId) return;
const record = getHomework(initialRecordId);
if (record) setView({ kind: "record", record });
}, [initialRecordId]);
/** Everything the API needs, built from the current input. */
const buildPayload = () => {
const readyDocuments = intake.documents.filter((entry) => entry.status === "ready" && entry.extracted);

View file

@ -169,7 +169,7 @@ const en: Record<string, string> = {
/* make my homework ------------------------------------------------- */
"homework.nav": "Make my homework",
"homework.title": "Make my homework",
"homeworkHeroTitle": "Solve your homework",
"homework.subtitle":
"Paste the assignment, snap a photo or add a document — then choose whether I should just do it, or guide you through it.",
"homework.source.title": "1. The assignment",
@ -222,6 +222,16 @@ const en: Record<string, string> = {
"homework.history.guide": "Guide",
"homework.history.openTitle": "Open “{title}”",
"homework.history.deleteConfirm": "Delete this result?",
"homework.emptyTitle": "No homework yet",
"homework.emptyBody": "Solved assignments and step-by-step guides you save show up here.",
"homework.open": "Open",
"homework.steps.one": "{n} step",
"homework.steps.few": "{n} steps",
"homework.steps.many": "{n} steps",
/* library tabs ----------------------------------------------------- */
"tabDecks": "Study Decks",
"tabHomework": "Homework",
/* quick actions (library empty state + /create switcher) ----------- */
"emptyStateCreateDeck": "Study Deck",
@ -753,7 +763,7 @@ const cs: Record<string, string> = {
/* make my homework ------------------------------------------------- */
"homework.nav": "Udělej mi úkol",
"homework.title": "Udělej mi úkol",
"homeworkHeroTitle": "Vyřeš svůj úkol",
"homework.subtitle":
"Vlož zadání, vyfoť ho nebo přidej dokument — a pak si vyber, jestli to mám rovnou udělat, nebo tě tím provést.",
"homework.source.title": "1. Zadání",
@ -806,6 +816,16 @@ const cs: Record<string, string> = {
"homework.history.guide": "Návod",
"homework.history.openTitle": "Otevřít „{title}“",
"homework.history.deleteConfirm": "Smazat tento výsledek?",
"homework.emptyTitle": "Zatím žádné úkoly",
"homework.emptyBody": "Uložená řešení úkolů a návody krok za krokem se objeví tady.",
"homework.open": "Otevřít",
"homework.steps.one": "{n} krok",
"homework.steps.few": "{n} kroky",
"homework.steps.many": "{n} kroků",
/* library tabs ----------------------------------------------------- */
"tabDecks": "Studijní balíčky",
"tabHomework": "Úkoly",
/* quick actions (library empty state + /create switcher) ----------- */
"emptyStateCreateDeck": "Studijní balíček",

View file

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