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) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 11:02:43 +02:00
co-authored by Claude Opus 4.6
parent 35c1ef1b80
commit 9912bfa387
5 changed files with 160 additions and 4 deletions
+11
View File
@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"fake-indexeddb": "^6.2.5",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^6.0.0", "vite": "^6.0.0",
"vitest": "^3.0.0" "vitest": "^3.0.0"
@@ -1261,6 +1262,16 @@
"node": ">=12.0.0" "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": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+3 -2
View File
@@ -13,9 +13,10 @@
"lint:fix": "biome check --write ." "lint:fix": "biome check --write ."
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0",
"fake-indexeddb": "^6.2.5",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^6.0.0", "vite": "^6.0.0",
"vitest": "^3.0.0", "vitest": "^3.0.0"
"@biomejs/biome": "^1.9.0"
} }
} }
+4 -2
View File
@@ -3,13 +3,15 @@ import { describe, expect, it, beforeEach } from "vitest";
import { openDB, getProgress, saveProgress, getSettings, saveSettings } from "./db"; import { openDB, getProgress, saveProgress, getSettings, saveSettings } from "./db";
import type { Progress, Settings } from "../types"; import type { Progress, Settings } from "../types";
let testCounter = 0;
describe("IndexedDB wrapper", () => { describe("IndexedDB wrapper", () => {
let db: IDBDatabase; let db: IDBDatabase;
beforeEach(async () => { beforeEach(async () => {
// Each test gets a fresh database by using a unique name // Each test gets a fresh database by using a unique name
// fake-indexeddb/auto resets between tests in vitest testCounter++;
db = await openDB(); db = await openDB(`zauberwald-test-${testCounter}`);
}); });
it("openDB resolves to an IDBDatabase with 5 object store names", () => { it("openDB resolves to an IDBDatabase with 5 object store names", () => {
+70
View File
@@ -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<IDBDatabase> {
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<Progress | null> {
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<void> {
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<Settings | null> {
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<void> {
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);
});
}
+72
View File
@@ -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<string, string>; // 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";