diff --git a/Dockerfile b/Dockerfile index a8df0fe..31a4131 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=80 -ENV DATA_DIR=/app/data +ENV DATA_DIR=/app/store COPY package*.json ./ RUN npm install --omit=dev @@ -13,7 +13,7 @@ COPY index.html styles.css app.js ./ COPY data/ ./data/ COPY server/ ./server/ -RUN mkdir -p /app/data +RUN mkdir -p /app/store EXPOSE 80 diff --git a/app.js b/app.js index 0e75721..5b542da 100644 --- a/app.js +++ b/app.js @@ -11,18 +11,25 @@ class WortKraftApp { this.cardRevealed = false; this.lastAnswerStatus = null; // { isCorrect, message, userText } - this.activeStoryId = window.GRADED_STORIES[0].id; + const stories = window.GRADED_STORIES || []; + const scenarios = window.CONVERSATION_SCENARIOS || []; + + this.activeStoryId = stories[0]?.id || ""; this.showReaderTranslation = false; this.selectedReaderWord = null; this.quizSelection = {}; - this.activeScenarioId = window.CONVERSATION_SCENARIOS[0].id; - this.scenarioState = {}; // { [scenarioId]: { turnIndex, messages, usedTargetWords } } + this.activeScenarioId = scenarios[0]?.id || ""; + this.scenarioState = {}; this.spotlightWords = []; + this.pendingTranslations = new Set(); this.loadState(); + this.hoverTranslateEnabled = this.settings.hoverTranslateEnabled !== false; this.initScenarioStates(); + this.setupHoverListeners(); + this.updateHoverToggleUI(); this.refreshDailySpotlight(); this.renderAll(); this.syncFromServer(); @@ -38,7 +45,7 @@ class WortKraftApp { if (Array.isArray(parsed.vocab) && parsed.vocab.length > 0) { this.vocab = parsed.vocab; const existingIds = new Set(this.vocab.map(v => v.id)); - window.INITIAL_VOCABULARY.forEach(item => { + (window.INITIAL_VOCABULARY || []).forEach(item => { if (!existingIds.has(item.id)) { this.vocab.push(this.createVocabEntry(item)); } @@ -46,15 +53,18 @@ class WortKraftApp { } if (Array.isArray(parsed.customStories)) this.customStories = parsed.customStories; if (parsed.stats) this.stats = parsed.stats; - if (parsed.settings) this.settings = parsed.settings; + if (parsed.settings) { + this.settings = parsed.settings; + this.hoverTranslateEnabled = this.settings.hoverTranslateEnabled !== false; + this.updateHoverToggleUI(); + } localStorage.setItem(STORAGE_KEY, JSON.stringify(parsed)); this.renderAll(); } else { - // First run on server: push current default state to server this.pushStateToServer(); } } catch (_e) { - // Offline or opened as local file:// — localStorage handles persistence + // Offline or local file:// mode — localStorage handles persistence } } @@ -81,9 +91,8 @@ class WortKraftApp { try { const parsed = JSON.parse(saved); this.vocab = parsed.vocab || []; - // Merge any new default words if vocabulary.js was updated const existingIds = new Set(this.vocab.map(v => v.id)); - window.INITIAL_VOCABULARY.forEach(item => { + (window.INITIAL_VOCABULARY || []).forEach(item => { if (!existingIds.has(item.id)) { this.vocab.push(this.createVocabEntry(item)); } @@ -95,9 +104,11 @@ class WortKraftApp { correctChecks: 0, sentencesWritten: 0 }; - this.settings = parsed.settings || { + this.settings = { aiProvider: "builtin", - apiKey: "" + apiKey: "", + hoverTranslateEnabled: true, + ...(parsed.settings || {}) }; return; } catch (e) { @@ -105,12 +116,9 @@ class WortKraftApp { } } - // Initialize default state const now = Date.now(); - this.vocab = window.INITIAL_VOCABULARY.map((item, idx) => { + this.vocab = (window.INITIAL_VOCABULARY || []).map((item, idx) => { const entry = this.createVocabEntry(item); - // Make first 15 words immediately due today so user has a focused starting queue, - // and the rest available as new/due as well entry.srs.dueDate = idx < 18 ? now - 1000 : now; return entry; }); @@ -124,7 +132,8 @@ class WortKraftApp { }; this.settings = { aiProvider: "builtin", - apiKey: "" + apiKey: "", + hoverTranslateEnabled: true }; this.saveState(); } @@ -137,7 +146,7 @@ class WortKraftApp { repetition: 0, efactor: 2.5, dueDate: Date.now(), - status: "new", // 'new' | 'learning' | 'mastered' + status: "new", reviews: 0 } }; @@ -158,7 +167,124 @@ class WortKraftApp { } getAllStories() { - return [...this.customStories, ...window.GRADED_STORIES]; + return [...this.customStories, ...(window.GRADED_STORIES || [])]; + } + + // === HOVER-TO-TRANSLATE MODE === + toggleHoverTranslate() { + this.hoverTranslateEnabled = !this.hoverTranslateEnabled; + this.settings.hoverTranslateEnabled = this.hoverTranslateEnabled; + this.updateHoverToggleUI(); + this.hideHoverTooltip(); + this.saveState(); + if (this.currentView === "reader") { + this.renderReaderMain(); + } + this.showToast( + this.hoverTranslateEnabled + ? "🔍 Hover-Übersetzer aktiviert: Fahre mit der Maus über deutsche Wörter!" + : "🔍 Hover-Übersetzer deaktiviert." + ); + } + + updateHoverToggleUI() { + document.body.classList.toggle("hover-translate-on", this.hoverTranslateEnabled); + const btn = document.getElementById("hover-translate-toggle"); + const statusSpan = document.getElementById("hover-translate-status"); + if (btn) btn.classList.toggle("active", this.hoverTranslateEnabled); + if (statusSpan) statusSpan.textContent = this.hoverTranslateEnabled ? "AN" : "AUS"; + } + + setupHoverListeners() { + document.addEventListener("mouseover", e => { + if (!this.hoverTranslateEnabled) return; + const tokenEl = e.target.closest(".word-token"); + if (!tokenEl) return; + const word = tokenEl.dataset.word || tokenEl.textContent.trim(); + if (!word) return; + this.showHoverTooltipForElement(tokenEl, word); + }); + + document.addEventListener("mouseout", e => { + const tokenEl = e.target.closest(".word-token"); + if (!tokenEl) return; + this.hideHoverTooltip(); + }); + + window.addEventListener("scroll", () => this.hideHoverTooltip(), { passive: true }); + } + + showHoverTooltipForElement(el, rawWord) { + const tooltip = document.getElementById("hover-translate-tooltip"); + if (!tooltip) return; + + const story = this.currentView === "reader" + ? this.getAllStories().find(s => s.id === this.activeStoryId) + : null; + + const info = this.lookupWordInfo(rawWord, story); + const articleHtml = info.article + ? `${info.article}` + : ""; + + tooltip.innerHTML = ` +