freezmo/app/deck/[id]/page.tsx
Martin Dvorak dfa7b7502a Add full-screen API-key gate
In user-key mode a browser with no stored Ollama key now gets a polished
gate instead of the app shell: one masked input with a show/hide toggle,
a single Continue button, inline validation errors, and a "?" help sheet
with the steps to a free key.

- Validates the draft key through /api/health (real 1-token probe) before
  saving; invalid-key and network failures read differently.
- Key still lives only in this browser's localStorage; nothing server-side.
- Gate wraps /, /create and deck study; server-key and local-daemon modes
  skip it; removing the key in Settings brings it back.
- Removes the now-redundant dashboard/create API-key banner.
- Monochrome corner glows (dark near-black / light near-white), 24px card,
  48px field and button, gentle fade/slide-in, reduced-motion aware.
- Help modal: centred dialog on desktop, bottom sheet on mobile, closes via
  X, Escape or backdrop.
- README and browser suites updated (34 gate checks, 15 key-flow checks,
  12 shipped-default checks; ui/folders/docs/pwa/security/contrast green).
2026-10-06 21:20:22 +00:00

71 lines
2.2 KiB
TypeScript

"use client";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import { ApiKeyGate } from "@/components/ApiKeyGate";
import { StudyFeed } from "@/components/StudyFeed";
import { Button, Spinner } from "@/components/ui";
import type { Deck } from "@/lib/schemas";
import { getDeck } from "@/lib/storage";
export default function DeckPage() {
const params = useParams<{ id: string }>();
const id = typeof params.id === "string" ? params.id : "";
const [deck, setDeck] = useState<Deck | null>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!id) {
setLoaded(true);
return;
}
setDeck(getDeck(id) ?? null);
setLoaded(true);
}, [id]);
if (!loaded) {
return (
<main className="frost-bg grid min-h-dvh place-items-center">
<div className="relative flex items-center gap-3 text-sm font-semibold text-frost-800 dark:text-frost-100">
<Spinner className="size-5" /> Loading your deck…
</div>
</main>
);
}
if (!deck) {
return (
<main className="frost-bg grid min-h-dvh place-items-center px-4">
<div className="card relative max-w-md space-y-3 p-6 text-center">
<p className="text-4xl" aria-hidden>
🧊
</p>
<h1 className="text-xl font-extrabold text-frost-950 dark:text-frost-50">Deck not found</h1>
<p className="text-sm text-slate-600 dark:text-slate-300">
This deck isn&apos;t in this browser&apos;s library. It may have been deleted, or it was
created in a different browser profile.
</p>
<div className="flex justify-center gap-2 pt-1">
<Link
href="/"
className="inline-flex items-center gap-2 rounded-2xl bg-accent-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-accent-700"
>
Back to library
</Link>
<Link href="/create">
<Button variant="secondary">Create a deck</Button>
</Link>
</div>
</div>
</main>
);
}
return (
<ApiKeyGate>
<StudyFeed key={deck.id} deck={deck} />
</ApiKeyGate>
);
}