diff --git a/index.html b/index.html index 3eeeeed..ddffa58 100644 --- a/index.html +++ b/index.html @@ -80,7 +80,79 @@
-
+
+
+ +

Elternbereich

+ + +
+

Bitte Code eingeben:

+ + +
+ + + +
+
diff --git a/src/styles/main.css b/src/styles/main.css index cd5a23f..5116078 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -441,6 +441,179 @@ h3 { } } +/* Parent Area */ +.parent { + max-width: 600px; + margin: 0 auto; + padding: 1.5rem; +} + +.parent__back-btn { + background: none; + border: none; + font-family: var(--font-heading); + font-size: 1rem; + color: var(--text-warm); + cursor: pointer; + margin-bottom: 1rem; +} + +.parent__title { + font-family: var(--font-heading); + color: var(--text-dark); + margin-bottom: 1.5rem; +} + +.parent__gate { + text-align: center; + padding: 2rem; + font-family: var(--font-body); +} + +.parent__code-input { + font-size: 2rem; + text-align: center; + width: 120px; + padding: 0.5rem; + border: 2px solid var(--accent-purple); + border-radius: 8px; + font-family: var(--font-heading); + letter-spacing: 0.5rem; + margin-top: 0.5rem; +} + +.parent__gate-error { + color: var(--accent-pink); + margin-top: 0.5rem; + font-size: 0.9rem; +} + +.parent__section { + margin-bottom: 2rem; +} + +.parent__section h2 { + font-family: var(--font-heading); + color: var(--text-dark); + border-bottom: 2px solid var(--bg-forest); + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} + +.parent__stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.parent__stat { + background: var(--bg-forest); + border-radius: 12px; + padding: 1rem; + text-align: center; +} + +.parent__stat-label { + display: block; + font-family: var(--font-body); + font-size: 0.85rem; + color: var(--text-warm); + margin-bottom: 0.25rem; +} + +.parent__stat-value { + display: block; + font-family: var(--font-heading); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-dark); +} + +.parent__calendar-dots { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 0.5rem; +} + +.parent__calendar-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent-green); +} + +.parent__calendar-dot--empty { + background: var(--bg-cream); + border: 1px solid var(--text-light); +} + +.parent__errors h3, +.parent__calendar h3 { + font-family: var(--font-heading); + font-size: 1rem; + color: var(--text-warm); + margin-bottom: 0.5rem; +} + +.parent__error-key { + display: inline-block; + background: var(--accent-pink); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 8px; + margin: 0.25rem; + font-family: var(--font-heading); + font-size: 0.9rem; +} + +.parent__settings { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.parent__setting { + display: flex; + align-items: center; + gap: 1rem; +} + +.parent__setting label:first-child { + min-width: 120px; + font-family: var(--font-body); + color: var(--text-warm); +} + +.parent__setting select, +.parent__input { + font-family: var(--font-body); + padding: 0.5rem; + border: 1px solid var(--text-light); + border-radius: 8px; + font-size: 0.95rem; +} + +.parent__save-btn { + font-family: var(--font-heading); + padding: 0.5rem 1rem; + background: var(--accent-green); + color: white; + border: none; + border-radius: 8px; + cursor: pointer; +} + +.parent__saved-feedback { + color: var(--accent-green); + font-family: var(--font-body); + font-size: 0.85rem; + margin-left: 0.5rem; + opacity: 1; + transition: opacity 0.3s ease; +} + /* Keyboard */ .keyboard { display: flex; diff --git a/src/ui/parent.ts b/src/ui/parent.ts new file mode 100644 index 0000000..d77a393 --- /dev/null +++ b/src/ui/parent.ts @@ -0,0 +1,229 @@ +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); +} + +/** + * 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, + 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, + apiKey: "", + apiCallsToday: { text: 0, image: 0 }, + lastApiCallDate: "", + }; + currentSettings.apiKey = apiKeyInput.value; + await saveSettings(db, currentSettings); + showSavedFeedback(saveApiKeyBtn); + }; +} + +/** + * 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); +}