From 9912bfa3873bbf9f0376708fc6209387a01f3b3b Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Sun, 29 Mar 2026 11:02:43 +0200 Subject: [PATCH] feat(01-01): implement TypeScript interfaces and IndexedDB wrapper - All shared interfaces: Progress, Settings, ForestElement, CompanionAssets, etc. - IndexedDB wrapper with 5 stores (progress, companionAssets, styleReference, forestElements, settings) - CRUD for progress and settings stores - 5 passing tests with fake-indexeddb Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 11 +++++++ package.json | 5 +-- src/storage/db.test.ts | 6 ++-- src/storage/db.ts | 70 ++++++++++++++++++++++++++++++++++++++++ src/types.ts | 72 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/storage/db.ts create mode 100644 src/types.ts diff --git a/package-lock.json b/package-lock.json index a4a5bb6..ba11f83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "devDependencies": { "@biomejs/biome": "^1.9.0", + "fake-indexeddb": "^6.2.5", "typescript": "~5.8.0", "vite": "^6.0.0", "vitest": "^3.0.0" @@ -1261,6 +1262,16 @@ "node": ">=12.0.0" } }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", diff --git a/package.json b/package.json index d6d1d4e..0496578 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,10 @@ "lint:fix": "biome check --write ." }, "devDependencies": { + "@biomejs/biome": "^1.9.0", + "fake-indexeddb": "^6.2.5", "typescript": "~5.8.0", "vite": "^6.0.0", - "vitest": "^3.0.0", - "@biomejs/biome": "^1.9.0" + "vitest": "^3.0.0" } } diff --git a/src/storage/db.test.ts b/src/storage/db.test.ts index 7b345b3..9704079 100644 --- a/src/storage/db.test.ts +++ b/src/storage/db.test.ts @@ -3,13 +3,15 @@ import { describe, expect, it, beforeEach } from "vitest"; import { openDB, getProgress, saveProgress, getSettings, saveSettings } from "./db"; import type { Progress, Settings } from "../types"; +let testCounter = 0; + describe("IndexedDB wrapper", () => { let db: IDBDatabase; beforeEach(async () => { // Each test gets a fresh database by using a unique name - // fake-indexeddb/auto resets between tests in vitest - db = await openDB(); + testCounter++; + db = await openDB(`zauberwald-test-${testCounter}`); }); it("openDB resolves to an IDBDatabase with 5 object store names", () => { diff --git a/src/storage/db.ts b/src/storage/db.ts new file mode 100644 index 0000000..5e67dd8 --- /dev/null +++ b/src/storage/db.ts @@ -0,0 +1,70 @@ +import type { Progress, Settings } from "../types"; + +const DB_NAME = "zauberwald"; +const DB_VERSION = 1; + +export function openDB(name: string = DB_NAME): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name, DB_VERSION); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains("progress")) { + db.createObjectStore("progress", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("companionAssets")) { + db.createObjectStore("companionAssets", { keyPath: "characterType" }); + } + if (!db.objectStoreNames.contains("styleReference")) { + db.createObjectStore("styleReference", { keyPath: "id" }); + } + if (!db.objectStoreNames.contains("forestElements")) { + db.createObjectStore("forestElements", { keyPath: "id", autoIncrement: true }); + } + if (!db.objectStoreNames.contains("settings")) { + db.createObjectStore("settings", { keyPath: "id" }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +export function getProgress(db: IDBDatabase): Promise { + return new Promise((resolve, reject) => { + const tx = db.transaction("progress", "readonly"); + const store = tx.objectStore("progress"); + const request = store.get(1); + request.onsuccess = () => resolve((request.result as Progress) ?? null); + request.onerror = () => reject(request.error); + }); +} + +export function saveProgress(db: IDBDatabase, progress: Progress): Promise { + return new Promise((resolve, reject) => { + const tx = db.transaction("progress", "readwrite"); + const store = tx.objectStore("progress"); + const request = store.put(progress); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export function getSettings(db: IDBDatabase): Promise { + return new Promise((resolve, reject) => { + const tx = db.transaction("settings", "readonly"); + const store = tx.objectStore("settings"); + const request = store.get(1); + request.onsuccess = () => resolve((request.result as Settings) ?? null); + request.onerror = () => reject(request.error); + }); +} + +export function saveSettings(db: IDBDatabase, settings: Settings): Promise { + return new Promise((resolve, reject) => { + const tx = db.transaction("settings", "readwrite"); + const store = tx.objectStore("settings"); + const request = store.put(settings); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5bc6b4f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,72 @@ +// Companion types +export type CompanionType = "fee" | "einhorn" | "fuchs" | "eule"; +export type KeyboardLayout = "de" | "ch"; + +// Level status for the forest map +export type LevelStatus = "locked" | "current" | "completed"; + +// Progress store +export interface Progress { + id: 1; // singleton row + currentLevel: number; + completedLevels: number[]; + totalSessions: number; + sessionDates: string[]; // ISO date strings (days only) + selectedCharacter: CompanionType; + selectedLayout: KeyboardLayout; +} + +// Companion assets store +export interface CompanionAssets { + characterType: CompanionType; + characterSheet: Blob; + avatarImage: Blob; + generatedAt: string; // ISO date +} + +// Style reference store +export interface StyleReference { + id: 1; // singleton + imageBlob: Blob; + generatedAt: string; +} + +// Forest element store +export interface ForestElement { + id?: number; // auto-increment + levelCompleted: number; + imageBlob: Blob; + imagePrompt: string; + description: string; + companionText: string; + position: { x: number; y: number }; + createdAt: string; +} + +// Settings store +export interface Settings { + id: 1; // singleton + audioEnabled: boolean; + apiKey: string; +} + +// Level definition (used in game/levels.ts) +export interface Level { + level: number; + newKeys: string[]; + allKeys: string[]; // cumulative keys up to this level + fingerMap: Record; // key -> CSS variable name for finger color +} + +// Typing exercise state +export interface TypingExerciseState { + currentLetterIndex: number; + letters: string[]; + intervalMs: number; + correctStreak: number; + totalCorrect: number; + totalErrors: number; +} + +// Screen names +export type ScreenName = "welcome" | "forest" | "lesson" | "reward" | "parent";