import { getProgress, saveProgress, getSettings, saveSettings } from "../storage/db"; import { companions } from "../companion/characters"; import type { CompanionType, KeyboardLayout, Settings } from "../types"; import { showScreen, getDB } from "../app"; const ACCESS_CODE = "1234"; /** * Show the parent code prompt gate, then reveal content on correct code. */ export function showParentCodePrompt(): void { showScreen("parent"); const gate = document.getElementById("parent-gate")!; const content = document.getElementById("parent-content")!; const codeInput = document.getElementById("parent-code-input") as HTMLInputElement; const gateError = document.getElementById("parent-gate-error")!; // Reset state gate.style.display = "block"; content.style.display = "none"; codeInput.value = ""; gateError.style.display = "none"; codeInput.focus(); // Remove previous listener to avoid duplicates const handler = () => { const value = codeInput.value; if (value === ACCESS_CODE) { gate.style.display = "none"; content.style.display = "block"; loadParentData(); codeInput.removeEventListener("input", handler); } else if (value.length === 4) { gateError.style.display = "block"; setTimeout(() => { gateError.style.display = "none"; codeInput.value = ""; }, 1500); codeInput.removeEventListener("input", handler); // Re-attach after clearing setTimeout(() => { codeInput.addEventListener("input", handler); }, 1500); } }; codeInput.addEventListener("input", handler); } /** * Initialize the parent screen: wire back button. */ export function initParentScreen(_db: IDBDatabase, onBack: () => void): void { const backBtn = document.getElementById("parent-back-btn"); if (backBtn) { backBtn.onclick = () => onBack(); } } /** * Load and display all parent data (stats + settings). */ async function loadParentData(): Promise { const db = getDB(); const progress = await getProgress(db); const settings = await getSettings(db); // Stats if (progress) { document.getElementById("stat-level")!.textContent = String(progress.currentLevel); document.getElementById("stat-sessions")!.textContent = String(progress.totalSessions); const total = progress.totalCorrect + progress.totalErrors; const accuracy = total > 0 ? Math.round((progress.totalCorrect / total) * 100) : 0; document.getElementById("stat-accuracy")!.textContent = `${accuracy}%`; // Calendar dots (last 30 days) renderCalendarDots(progress.sessionDates); // Error keys renderErrorKeys(progress.errorKeyCounts); } // Settings await loadSettings(db, progress, settings); // Wire data action buttons document.getElementById("export-btn")!.onclick = () => exportData(db); document.getElementById("import-btn")!.onclick = () => (document.getElementById("import-file") as HTMLInputElement).click(); document.getElementById("import-file")!.addEventListener("change", (e) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) importData(db, file); }); document.getElementById("reset-btn")!.onclick = () => resetAllData(db); } /** * Render calendar dots for the last 30 days. */ function renderCalendarDots(sessionDates: string[]): void { const container = document.getElementById("calendar-dots")!; container.innerHTML = ""; const today = new Date(); const sessionSet = new Set(sessionDates); for (let i = 29; i >= 0; i--) { const date = new Date(today); date.setDate(today.getDate() - i); const dateStr = date.toISOString().split("T")[0]!; const dot = document.createElement("div"); dot.className = sessionSet.has(dateStr) ? "parent__calendar-dot" : "parent__calendar-dot parent__calendar-dot--empty"; dot.title = dateStr; container.appendChild(dot); } } /** * Render error key badges, sorted by frequency, top 5. */ function renderErrorKeys(errorKeyCounts: Record): void { const container = document.getElementById("error-keys-list")!; container.innerHTML = ""; const entries = Object.entries(errorKeyCounts).sort((a, b) => b[1] - a[1]); if (entries.length === 0) { container.textContent = "Noch keine Fehler erfasst"; return; } const top5 = entries.slice(0, 5); for (const [key, count] of top5) { const badge = document.createElement("span"); badge.className = "parent__error-key"; badge.textContent = `${key.toUpperCase()} (${count}x)`; container.appendChild(badge); } } /** * Load settings into form elements and wire change handlers. */ async function loadSettings( db: IDBDatabase, progress: Awaited>, settings: Settings | null, ): Promise { const layoutSelect = document.getElementById("setting-layout") as HTMLSelectElement; const audioCheckbox = document.getElementById("setting-audio") as HTMLInputElement; const companionSelect = document.getElementById("setting-companion") as HTMLSelectElement; const apiKeyInput = document.getElementById("setting-apikey") as HTMLInputElement; const saveApiKeyBtn = document.getElementById("save-apikey-btn")!; // Populate companion select companionSelect.innerHTML = ""; for (const c of companions) { const option = document.createElement("option"); option.value = c.type; option.textContent = `${c.emoji} ${c.name}`; companionSelect.appendChild(option); } // Set current values if (progress) { layoutSelect.value = progress.selectedLayout; companionSelect.value = progress.selectedCharacter; } if (settings) { audioCheckbox.checked = settings.audioEnabled; apiKeyInput.value = settings.apiKey || ""; } // Wire change handlers layoutSelect.onchange = async () => { if (!progress) return; progress.selectedLayout = layoutSelect.value as KeyboardLayout; await saveProgress(db, progress); showSavedFeedback(layoutSelect); }; companionSelect.onchange = async () => { if (!progress) return; progress.selectedCharacter = companionSelect.value as CompanionType; await saveProgress(db, progress); showSavedFeedback(companionSelect); }; audioCheckbox.onchange = async () => { const currentSettings = settings ?? { id: 1 as const, audioEnabled: false, speechEnabled: false, apiKey: "", apiCallsToday: { text: 0, image: 0 }, lastApiCallDate: "", }; currentSettings.audioEnabled = audioCheckbox.checked; await saveSettings(db, currentSettings); showSavedFeedback(audioCheckbox); }; saveApiKeyBtn.onclick = async () => { const currentSettings = settings ?? { id: 1 as const, audioEnabled: false, speechEnabled: false, apiKey: "", apiCallsToday: { text: 0, image: 0 }, lastApiCallDate: "", }; currentSettings.apiKey = apiKeyInput.value; await saveSettings(db, currentSettings); showSavedFeedback(saveApiKeyBtn); }; } const DB_STORES = ["progress", "companionAssets", "styleReference", "forestElements", "settings"]; /** * Show feedback message in the data section. */ function showDataFeedback(message: string, isError = false): void { const el = document.getElementById("data-feedback")!; el.textContent = message; el.style.display = "block"; el.style.color = isError ? "#D46A6A" : ""; setTimeout(() => { el.style.display = "none"; }, 3000); } /** * Export progress and settings as a JSON file download. */ async function exportData(db: IDBDatabase): Promise { const progress = await getProgress(db); const settings = await getSettings(db); const data = { version: 1, exportedAt: new Date().toISOString(), progress, settings, }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "zauberwald-backup.json"; a.click(); URL.revokeObjectURL(url); showDataFeedback("Exportiert!"); } /** * Import progress and settings from a JSON file. */ async function importData(db: IDBDatabase, file: File): Promise { try { const text = await file.text(); const parsed = JSON.parse(text); // Validate structure if ( !parsed.progress || typeof parsed.progress.currentLevel !== "number" || !Array.isArray(parsed.progress.completedLevels) || typeof parsed.progress.totalSessions !== "number" || typeof parsed.progress.selectedCharacter !== "string" || typeof parsed.progress.selectedLayout !== "string" ) { showDataFeedback("Ungueltige Datei", true); return; } if (!parsed.settings || typeof parsed.settings.audioEnabled !== "boolean") { showDataFeedback("Ungueltige Datei", true); return; } if (!confirm("Fortschritt überschreiben? Aktuelle Daten gehen verloren.")) { return; } await saveProgress(db, { ...parsed.progress, id: 1 }); await saveSettings(db, { ...parsed.settings, id: 1 }); showDataFeedback("Importiert!"); setTimeout(() => location.reload(), 1500); } catch { showDataFeedback("Ungueltige Datei", true); } } /** * Clear a single IndexedDB object store. */ function clearStore(db: IDBDatabase, storeName: string): Promise { return new Promise((resolve, reject) => { const tx = db.transaction(storeName, "readwrite"); const request = tx.objectStore(storeName).clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } /** * Reset all data with double confirmation, then reload. */ async function resetAllData(db: IDBDatabase): Promise { if (!confirm("Wirklich alles löschen?")) return; if (!confirm("Alle Fortschritte und Einstellungen werden gelöscht. Bist du sicher?")) return; await Promise.all(DB_STORES.map((store) => clearStore(db, store))); location.reload(); } /** * Show brief "Gespeichert!" feedback next to an element. */ function showSavedFeedback(element: HTMLElement): void { // Remove any existing feedback const existing = element.parentElement?.querySelector(".parent__saved-feedback"); if (existing) existing.remove(); const feedback = document.createElement("span"); feedback.className = "parent__saved-feedback"; feedback.textContent = "Gespeichert!"; element.parentElement?.appendChild(feedback); setTimeout(() => { feedback.style.opacity = "0"; setTimeout(() => feedback.remove(), 300); }, 1500); }