A minimal spaced repetition flashcard app built with Astro, React, and Tailwind CSS.
- Deck management - create, edit, and delete decks
- Card management - add, edit, and remove cards per deck
- Spaced repetition - SM-2 algorithm schedules reviews automatically
- Study session - flip cards, rate recall (Again / Hard / Good / Easy)
- Persistent storage - all data lives in
localStorage, no backend needed - Client-side routing - SPA navigation without full reloads
| Layer | Choice |
|---|---|
| Framework | Astro 6 (static output) |
| UI | React 19 (islands) |
| Styling | Tailwind CSS v4 |
| Components | Radix UI primitives + CVA |
| Icons | Lucide React |
| Storage | localStorage |
npm install
npm run devOpen http://localhost:4321.
| Command | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Build for production |
npm run preview |
Preview production build |
src/
├── components/
│ ├── ui/ # Low-level UI primitives (Button, Card, Dialog, ...)
│ ├── App.tsx # Root component + client-side router
│ ├── Dashboard.tsx
│ ├── DeckView.tsx
│ ├── DeckForm.tsx
│ ├── CardForm.tsx
│ └── StudySession.tsx
├── layouts/
│ └── Layout.astro
├── lib/
│ ├── sm2.ts # SM-2 spaced repetition algorithm
│ ├── store.ts # localStorage data layer
│ ├── navigate.ts # SPA navigation helper
│ ├── types.ts # Shared TypeScript types
│ └── utils.ts
├── pages/
│ └── index.astro # Single entry point
└── styles/
└── globals.css
Each card tracks three values:
- interval - days until the next review
- repetitions - number of successful reviews
- easeFactor - difficulty multiplier (starts at 2.5, min 1.3)
After each review, the rating maps to a quality score and updates the schedule:
| Rating | Quality | Effect |
|---|---|---|
| Again | 0 | Reset to 0 repetitions, review in 1 day |
| Hard | 2 | Advance slowly, ease decreases |
| Good | 4 | Advance normally |
| Easy | 5 | Advance fast, ease increases |
type Deck = {
id: string
name: string
description: string
createdAt: number
updatedAt: number
}
type Card = {
id: string
deckId: string
front: string
back: string
interval: number // days until next review
repetitions: number
easeFactor: number // starts at 2.5, min 1.3
dueDate: number // Unix timestamp ms
createdAt: number
updatedAt: number
}Client-side routing is handled entirely in App.tsx by listening to popstate and intercepting <a> clicks. No router library is used.
| Path | View |
|---|---|
/ |
Dashboard - all decks |
/deck/:id |
Deck view - card list |
/study/:id |
Study session |