Skip to content

nangelovv/Dutch

Repository files navigation

Dutch Visual Mastery

Visual Dutch vocabulary trainer with SRS, eight exercise modes, offline PWA support, and optional cross-device sync via private GitHub Gist.

Built with Vite + React 18 (plain JS) + Tailwind. State lives in Zustand, sync uses TanStack Query, animations are Framer Motion, the service worker is generated by Workbox via vite-plugin-pwa.

Quickstart

npm install
npm run dev          # http://localhost:5173
npm run build        # → dist/
npm run preview
npm test             # vitest

GitHub Pages deploy

The included workflow at .github/workflows/deploy.yml builds and publishes on every push to main using the official actions/deploy-pages@v4 action.

  1. Push this repo to GitHub.
  2. In Settings → Pages, set Source = GitHub Actions.
  3. The workflow infers the base path from the repo name and deploys to https://<user>.github.io/<repo>/.

To deploy somewhere else (custom domain, project root, subfolder), set the VITE_BASE_PATH env var when building:

VITE_BASE_PATH=/ npm run build           # root deploy
VITE_BASE_PATH=/dutch/ npm run build     # subfolder deploy

The default in vite.config.js is /dutch-visual-mastery/. The app uses HashRouter, so deep links survive the GitHub Pages 404 problem with no extra configuration.

Sync via GitHub Gist

Sync is opt-in and runs entirely from the browser — no backend.

  1. Create a fine-grained Personal Access Token with the gist scope (only). https://github.com/settings/tokens
  2. Open Settings → Sync via GitHub Gist in the app, paste the token, and tap Connect GitHub.
  3. The app creates a private gist named dutch-visual-mastery-state.json on first connect and merges it on every sync.

Token storage warning: the PAT is stored in this device's localStorage. Anyone with access to the browser profile can read or use it. Use a token scoped only to gist, and tap Disconnect & wipe token when finished.

Auto-sync triggers: app start, end of every session, every 5 minutes while the tab is focused, and on the online event. There's also a manual Sync now button.

If you don't want to use Gist, the same Settings panel offers JSON Export and Import for manual transfer.

Features

  • 8 exercise modes — Image → Dutch, Dutch → English, English → Dutch, Article (de/het), Type the word, Listening, Sentence cloze, Reverse cloze. Disable individual modes in Settings; the picker weights toward your weakest active mode.
  • Leitner-box SRS — 5 boxes with intervals 10 min / 1 d / 3 d / 7 d / 21 d. Three consecutive easy correct in box 5 → mastered.
  • Revision Mode — sidebar toggle that restricts sessions to words you rated medium or hard.
  • Streaks & achievements — daily streak with one grace day per rolling 7 days; toast on each unlock.
  • PWA — installable, offline-capable, image cache, generated icons.
  • Notifications — opt-in reminders at user-configured times.
  • Stats — words-per-level stacked bar, accuracy by mode, 30-day GitHub-style heatmap.
  • Keyboard1-4 choose, Space audio, E/M/H rate, Enter advance, Esc exit a session.

Project structure

src/
├── main.jsx, App.jsx
├── routes/        Dashboard, Quiz, WordList, Stats, Settings
├── components/    Sidebar, BottomNav, ProgressRing, Heatmap,
│                  SpeakerButton, SyncStatus
│   └── quiz/      8 mode components + shared QuizCard / ChoiceGrid /
│                  ImageCard / useAnswerFlow
├── hooks/         useTTS, useNotifications, useGistSync, useKeyboardShortcuts
├── lib/           srs, quiz, merge, levenshtein, gist, storage, types (JSDoc)
├── store/         progress, settings, session  (Zustand)
├── data/          words-level1.json (+ index.js merger)
└── styles/        index.css   (Tailwind directives only)
tests/             srs / levenshtein / merge   (Vitest, jsdom)
.github/workflows/ deploy.yml
public/icons/      icon.svg, icon-maskable.svg

Notes on browser features

Feature Tab open Installed PWA Background
TTS audio n/a
Auto-sync (5 min) ✅ (when foregrounded)
Reminders at fixed times ✅ (in-app) ✅ (in-app) ⚠️ depends on periodicSync support — Chrome on Android only at the time of writing
Image caching n/a

useTTS is built around the Web Speech API; on first install Dutch voices may not be available immediately — the hook listens for voiceschanged and caches the chosen voice. If neither nl-NL nor nl-BE is found, the speaker button remains disabled rather than speaking with the wrong locale.

Adding more vocabulary

The repo ships with level 1 (top 100 most common words) only. To add more levels, drop another file in src/data/:

// src/data/words-level2.json
[
  { "id": "l2-001", "en": "...", "nl": "...", "article": "de"|"het"|null,
    "pos": "noun", "level": 2, "imageKeyword": "..." }
]

…then import it in src/data/index.js:

import level2 from './words-level2.json';
const levels = { 1: level1, 2: level2, /* ... */ };

The Word shape lives in src/lib/types.js (JSDoc, no compile step).

Code quality

  • All pure logic (srs, quiz, merge, levenshtein) is in src/lib/ and unit-tested with Vitest under tests/. CI runs the suite before deploy.
  • All localStorage reads/writes go through src/lib/storage.js and are wrapped in try/catch with sensible defaults — corrupt data does not crash the app.
  • ESLint is configured (.eslintrc.cjs); npm run lint should produce no warnings on a clean tree.
  • Image loads have a graceful text-only fallback on 404.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages