Freezmo: self-hosted notes-to-activities study app
Next.js 15 app that turns photos, PDFs, DOCX/PPTX and text into a swipeable feed of AI-generated activities (flashcards, multiple choice, true/false, fill-in-the-blank, matching, ordering, short answer) using the user's own Ollama models. Highlights: - All AI calls run in server route handlers; the browser never talks to Ollama. - Bring-your-own-key mode (default): the key lives only in the browser and is sent per-request; server, local-daemon and shared-key modes also supported. - Client-side document extraction (pdf.js, mammoth, jszip) with scanned-PDF page rendering for the vision model; notes stay in their original language. - ChatGPT-style shell: collapsible sidebar / mobile drawer, neutral token-based light + dark themes, folder organisation, PWA (manifest, SW, offline page). - Strict JSON validation with zod + auto-repair, rate limiting, and request guards (cross-origin, body-size, API-key sanitising). - setup.sh bootstraps deps, .env.local and a production build. Gitignore excludes .env*, the generated pdf.js worker and local scratch files.
This commit is contained in:
parent
3baa0fa8ce
commit
3ea2cea100
83 changed files with 10909 additions and 165 deletions
66
app/deck/[id]/page.tsx
Normal file
66
app/deck/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
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't in this browser'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 <StudyFeed key={deck.id} deck={deck} />;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue