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",
|
"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
@@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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