paulserban.eu

Portfolio Edition

Paul Serban

AI & full-stack engineer · agentic platforms · distributed systems

← Back to portfolio

Featured

Spaced-Repetition Quiz Engine

Flashcards that schedule themselves

Role: Architect & lead implementer

  • #fsrs
  • #pwa
  • #react
  • #tanstack router
  • #typescript

Problem

Studying from personal notes meant a backend, an account, or a paid SaaS - with no algorithm swap and no reuse of the blog's content.

Approach

Designed the quiz as a fourth static consumer of content.db - quiz-export to JSON v2, CSR React PWA with Zustand progress, shared UI blocks, and a dual SM-2/FSRS scheduler behind one interface.

Outcome

Offline-capable study tool that reuses blog content with zero duplication, lossless algorithm migration, and a harness that compares review load at the same retention target.

schedulers
2
answer_formats
4
accounts_required
0
View code Live demo

Problem

I wanted to study from my own engineering notes without a backend, an account, or a paid flashcard SaaS - and I wanted to swap scheduling algorithms without rewriting the product. Existing tools either lock content behind proprietary formats or ship a single opaque scheduler with no way to measure alternatives.

The knowledge already lived next to the blog. Duplicating it into Anki decks or a third-party app would fork the corpus: tags drift, questions detach from the posts they teach, and "update the article" no longer means "update the study set."

The real design problem was not another flashcard UI. It was a platform seam: author once in the content repo, deliver a study surface that stays offline-capable, algorithm-pluggable, and honest about what "zero accounts" costs.

Solution

The quiz is the fourth static consumer of prj--personal-portfolio--v3's shared content artifact. Questions are authored as MDX co-located with posts (…/{slug}/questions/{slug}--{uid}.mdx), ingested into the same SQLite content.db as the Astro sites, then compiled by tools--quiz-export into versioned static JSON.

The web app is a strict CSR React 19 + Vite PWA ("The Typeset Review"): it fetches JSON shards, tracks learning state in the browser (Zustand + versioned localStorage), and rates cards through a pluggable Scheduler interface. Presentation lives as Storybook-backed blocks in shared--ui; this app owns containers, hooks, routes, store, and algorithms only.

No runtime database. No user accounts. Content and progress never share a blob - progress is keyed by question slug so re-exports and set adds do not orphan schedules.

Architecture

AUTHOR  content--paulserban.eu
  publish/…/{slug}.mdx
  publish/…/questions/{slug}--{uid}.mdx
        │  content-sync -> mdx-ingest
        ▼
  content.db  (same artifact as portfolio / blog / news-feed)
        │  tools--quiz-export  (JSON contract v2)
        ▼
  public/data/
    posts.json · tags.json          <- PWA precache indexes
    questions/<post>.json           <- lazy set shards
    tags/<tag>.json · _all.json     <- tag study + full offline
        │  fetch() + vite-plugin-pwa
        ▼
  CSR React PWA  (quiz.paulserban.eu)
    ├── TanStack Router (browse / sets / tags / study / stats / settings)
    ├── Zustand persist  key quiz-web-app:v1
    ├── Scheduler seam: SM-2 ⇄ FSRS-5  (lossless migrate)
    ├── shared--ui blocks (StudyCard, QuestionRenderer, …)
    └── Stats + backup / restore
LayerResponsibility
shared--question-contractZod frontmatter; answer_format × cognitive_style; derived grading_mode
tools--mdx-ingestCo-located questions/ -> questions / question_options with FK to parent post
tools--quiz-exportDB -> compiled HTML stems -> sharded JSON v2 + copied assets
shared--markdownOne sanitize / compile path (DOMPurify allow-list) for export and client
shared--ui blocksPure props-in / callbacks-out study chrome; Storybook-backed
quiz-web-appRoutes, containers, hooks, store, schedulers, PWA shell

Approach

As architect and lead implementer, I treated the quiz as a delivery boundary on the content platform - not a standalone product that invents its own corpus.

Platform boundaries first

Scheduler as a product decision

Study product surface

Stack

LayerChoices
AuthoringMDX questions co-located with posts; Zod question contract
Pipelinemdx-ingest -> content.db -> quiz-export JSON v2
AppReact 19, Vite, TanStack Router, Zustand persist, Tailwind / newspaper DS
AlgorithmsSM-2 + FSRS-5 behind Scheduler; shared learning FSM + fuzz
Offlinevite-plugin-pwa, precache indexes, SWR for /data/*
HostingStatic CDN origin at quiz.paulserban.eu

Design decisions that mattered

What shipped

Outcome

A genuinely offline-capable study tool that reuses the blog's content with zero duplication, treats the scheduler as a swappable seam with measurable trade-offs, and keeps "zero accounts" as an architectural choice rather than a missing feature. The quiz is not a side project bolted on - it is another delivery shape of the same content platform contract: author once, fan out many times.

Part of prj--personal-portfolio--v3

This PWA is the study surface of the monorepo. Related pieces: