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:
Generated
+11
@@ -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",
|
||||
|
||||
+3
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user