0. CONTENT GENERATION --------------------- Generate a list of 20 interesting German compound words that don't have a direct translation in English. Example: Schadenfreude. Exclude German words longer than 24 letters. 1. GLOBAL CONFIGURATION ----------------------- - CARDS_PER_ROUND: 9 - BASE_LANGUAGE: English - TARGET_LANGUAGE: German - LANGUAGE_CODE: Two letter standard code for TARGET_LANGUAGE (All Caps). - GA_MEASUREMENT_ID: "G-WFWF326Q9H" Dataset Fields: id | text (German) | translation (English Translation. Include the literal translation of the component words and the idiomatic meaning. Label the literal translation "Literally". Do not label the idiomatic translation.) | audio (set explicitly to null) Elminate any duplicates in the dataset. Act as an expert Frontend Engineer. Build a single, standalone HTML file (index.html) for a language learning game. IMPORTANT: Do not add any notes at the top or bottom of the HTML file. 2. TECHNICAL STACK & SETUP -------------------------- - React, ReactDOM, Babel, TailwindCSS (via CDN). - Embed all CSS/JS within the single file. - You MUST use the following exact script tags in the
for dependencies: - Analytics: Include the standard Google Analytics 4 (gtag.js) script in the . Initialize `window.dataLayer` and config using the `GA_MEASUREMENT_ID`. 3. ARCHITECTURE & STATE MANAGEMENT (CRITICAL) --------------------------------------------- - Storage Key: Use const `STORAGE_KEY = 'openlang_German_Longer-Phrases_progress'`. - Persistence Schema: `{ score: number, streak: number, learnedIds: [] }`. - Initialization Logic (Order of Operations): 1. On mount, read `localStorage`. 2. If data exists, hydrate `score` and `streak` state from it. 3. Filter `RAW_DATA` to exclude any IDs found in `learnedIds`. 4. Initialize `deckRef` using a Fisher-Yates shuffle of the *remaining* (unlearned) items. 5. If no items remain (all learned), trigger the Win Condition immediately. 6. If items remain, call `startRound()` to load the first batch. - Round State: Use `useState` (`roundData`) for the current CARDS_PER_ROUND pairs. - Mastery Logic: When a match is made (no hint), remove the word from `deckRef.current` immediately. - Round Transition (useEffect Pattern): - You MUST use a `useEffect` hook to handle transitions. Do NOT trigger the next round inside `handleMatch`. - Watch dependency: `[roundData.left.length, isWon]`. - Logic: IF `roundData.left.length === 0` AND `!isWon` AND `deckRef.current` is initialized: a. Check if `deckRef.current.length > 0`. b. If YES: Call `startRound()`. c. If NO: Set `isWon(true)`. - TARGET_LANGUAGE Column Logic: Must be a VALID DERANGEMENT (shuffled so no TARGET_LANGUAGE card lines up horizontally with its English counterpart). If batch size < 2, return as-is. 4. LAYOUT & VISUALS ------------------- - Master Container: Wrap the entire application in a single div. - Desktop: width: 400px; margin: 0 auto; position: relative; - Mobile: width: 100%; height: 100dvh; - Behavior: All children (Header, Game Area, Footer) must inherit this exact width. - Structure: A flex-column container (h-100dvh) within the master boundary. Header: Fixed height, flex-shrink: 0. Game Area: Flex-1, overflow: hidden, overscroll-behavior: none. (CRITICAL: Do NOT use overflow-y: auto. You MUST enforce the touch-none Tailwind class directly on the JSX elements to prevent entire columns from native dragging/scrolling). This container MUST have the class flex so its children (the two columns) automatically stretch to fill the full available height. Columns: Two columns (50% width). - CSS Logic: Apply flex flex-col justify-start to each column. Calculate a fixed card height using calc((100dvh - 6.9rem) / (CARDS_PER_ROUND + 1)). This ensures cards never change size or move positions when others are removed. Card Content (Strict): Left (English) Card: Display the English text using the calculated fixed height. Right (TARGET_LANGUAGE) Card: Display the TARGET_LANGUAGE translation using the calculated fixed height. Card Styling: Use p-2 (0.5rem) padding and leading-tight. Centrally align text vertically and horizontally. The white background must tightly 'hug' the text within the calculated height. MUST INCLUDE CSS: `user-select: none; -webkit-user-select: none;` to prevent native text-drag interference. Visual Hover State (PRO): Define a CSS class .target-hover. Style: border: 2px solid #3b82f6 !important; background-color: #eff6ff !important; transform: scale(1.05); transition: transform 0.1s ease;. This class will be applied to a TARGET_LANGUAGE card when an BASE_LANGUAGE card is "pointing" at it. Mobile View: UI must fit on a single screen without horizontal scrolling. Desktop: Set CSS width to 400px (including header and footer). Footer: Fixed height. Contains Copyright (defined below). 5. INTERACTION & EVENTS (STRICT CONSTRAINTS) -------------------------------------------- You must implement two completely separate event handling systems. Do not unify them. Use a `ref` (e.g., `clickTracker`) to store `{ id: null, time: 0 }` for double-action detection. System A: Desktop (Mouse) - Hint Event: Use the standard `onDoubleClick` React event to trigger the hint. CRITICAL FIX: You MUST conditionally disable this on mobile to prevent double-speak (e.g., `onDoubleClick={isTouchDevice ? undefined : () => handleHint(item)}`). If you leave it active on mobile, the OS will fire both the custom touch double-tap AND the native double-click, playing the audio twice! - Draggable Attribute: Conditionally set draggable={!isTouchDevice}. CRITICAL FIX: You MUST manage isTouchDevice using React useState (e.g., const [isTouchDevice, setIsTouchDevice] = useState(false)). Do NOT use useRef. Update it to true inside useEffect on mount. This guarantees React re-renders and strictly removes the draggable attribute on mobile, preventing native OS drag from hijacking the UI and dragging the whole column! - Events: Use onDragStart, onDrop, and onDoubleClick. - Visuals: Use onDragOver (apply .target-hover) and onDragLeave (remove .target-hover). System B: Mobile (Touch) - PRECISION GHOST CARD & HOVER IMPLEMENTATION CSS Requirement: You MUST add the Tailwind class touch-none directly to the JSX className of the .game-area container, the two column containers, AND every .game-card element. Do not rely on custom CSS blocks for this. Add onContextMenu={(e) => e.preventDefault()} to the card elements to prevent long-press ghost dragging. Logic Pattern: Use the "Global Window Listener" pattern. CRITICAL LISTENER LIFECYCLE: You MUST attach `touchstart`, `touchmove`, and `touchend` window listeners exactly once inside a `useEffect` on component mount. DO NOT attach the `touchmove` listener dynamically inside the `touchstart` handler, otherwise mobile browsers will ignore `e.preventDefault()`. Always use the `{ passive: false }` option. Double Tap Guard: At start of onTouchStart, check if id === ref.current.id and Date.now() - ref.current.time < 300. If YES: trigger hint, remove existing clones, and RETURN immediately. Clone Creation: Create visual CLONE appended to document.body. Set clone.style.width to source element's offsetWidth, pointer-events: none, and z-index: 9999. Visibility Offset: Set style.top to (touch.clientY - 90) + 'px' and style.left to (touch.clientX - (clone.offsetWidth / 2) - 30) + 'px'. This large offset ensures the card is fully visible above and to the left of the finger. The activeTouchMove function: CRITICAL ANTI-SCROLL FIX: At the VERY TOP of the function, you MUST add: if (e.target.closest('.game-area')) { e.preventDefault(); } BEFORE any guard clauses like if (!isDragging) return;. If you place preventDefault after the guard clause, touching the gaps between cards will drag/scroll the entire column! After your guard clause, update Clone top and left using the same offsets. Hover State Management (The "Pro" Standard): Calculate the clone's center point: const centerX = clone.offsetLeft + (clone.offsetWidth / 2); const centerY = clone.offsetTop + (clone.offsetHeight / 2); Use document.elementFromPoint(centerX, centerY) to find the element currently under the Clone. If the element is a TARGET_LANGUAGE card (or inside one), apply .target-hover to it. CRITICAL: Remove .target-hover from all other cards immediately so only one is highlighted at a time. CRITICAL CONSTRAINT: You MUST use direct DOM manipulation (element.classList.add/remove) to apply .target-hover. DO NOT use React state (useState) to track the hovered card during activeTouchMove. The activeTouchEnd function: Remove window listeners and the Clone. Precision Drop Logic: Identify the drop target using document.elementFromPoint() at the Clone's last center coordinates (not the finger coordinates). Remove .target-hover from all cards. Call handleMatch(id) or handleFail(id) based on the data-id of the target found. Critical: Inside the onTouchStart global listener, you MUST verify the target is a card using const sourceCard = e.target.closest('.game-card'). If no card is found, return immediately before creating any clones or preventing defaults. Cleanup: Ensure listeners and hover classes are removed even if the component unmounts. 6. GAME LOGIC ------------- - Matching: Drag BASE_LANGUAGE to TARGET_LANGUAGE. - HINTS: - Input: Triggered via the manual Double-Click/Tap logic defined in Section 5. - CRITICAL DEBOUNCE: Inside the function that executes the hint, include: `if (window._lastHintTime && Date.now() - window._lastHintTime < 500) return; window._lastHintTime = Date.now();`. - Effect: Set a GLOBAL variable `window._hintUsed = true;` (DO NOT use React useState for this, it will fail in touch closures). - Reset streak to 0. - Set a React state `hintTargetId` to the item.id. The TARGET_LANGUAGE card flashes Orange for 2 seconds. Speak the card. - SUCCESS (IDs match): - Execution Order (CRITICAL iOS TIMING): 1. Read `window._hintUsed`. 2. If false, IMMEDIATELY AND SYNCHRONOUSLY call `speakCard(matchedItem)`. 3. If false, update Score (+10 + Streak) and update `learnedIds`. Save to localStorage. 4. Increment Streak (if !hintUsed). 5. Set `window._hintUsed = false`. 6. Add baseId to a `successIds` array state to trigger a 1-second Green Flash via CSS. 7. CRITICAL: Wrap the actual removal of the cards from `roundData` in a `setTimeout` of 1000ms. - FAIL (IDs mismatch): - Add IDs to a `failIds` array state to trigger a 2-second Red Flash via CSS. - Set `window._hintUsed = false`. - Reset Streak to 0. Deduct 5 from Score. Save to localStorage. - Remove from `failIds` after 2000ms. 7. AUDIO SYSTEM (MOBILE ROBUSTNESS - iOS FIXES) ----------------------------------------------- CRITICAL: iOS Safari TTS is highly unstable. You MUST implement the audio system exactly as described below. Initialization & Unlock (The Native Pattern): - You MUST use a `useEffect` on component mount to attach native listeners: `window.addEventListener('touchstart', unlockAudio, { once: true });` and `mousedown`. - Inside `unlockAudio`: Call `window.speechSynthesis.resume()`. Create `const unlockUtterance = new SpeechSynthesisUtterance(' ');`. Set volume to 1. Call `window.speechSynthesis.speak(unlockUtterance);`. DO NOT place this unlock logic inside the game's drag-and-drop touch events. Implementation inside `speakCard(item)`: - STALE CLOSURE FIX: At the very top, add: `if (localStorage.getItem('openlang_global_mute_state') === 'true') return;`. - Target Locale: If `navigator.language.toLowerCase().includes('en')`, use `CONFIG.LANGUAGE_CODE.toLowerCase()`. Otherwise, use 'en'. - Text Source: If English browser, use TARGET_LANGUAGE. Otherwise, use English. Execution Constraints: - Priority 1: If `item.audio` exists, `new Audio(item.audio).play(); return;` - Priority 2 (Synthesis Fallback): 1. CRITICAL iOS QUEUE FIX: `window.speechSynthesis.cancel();` 2. `window.speechSynthesis.resume();` 3. Create: `const utterance = new SpeechSynthesisUtterance(TextSource);` 4. CRITICAL iOS GARBAGE COLLECTION FIX: `window._activeUtterance = utterance;` 5. `utterance.lang = TargetLocale;` 6. `const voices = window.speechSynthesis.getVoices();` 7. `const voiceMatch = voices.find(v => v.lang.toLowerCase().startsWith(TargetLocale));` 8. CRITICAL iOS LANG FIX: If `voiceMatch` exists, set `utterance.voice = voiceMatch;` AND `utterance.lang = voiceMatch.lang;` (This safely adopts the exact device tag, preventing silent drops). 9. Call `window.speechSynthesis.speak(utterance);` SYNCHRONOUSLY. 8. HEADER --------- - Header Elements LOGO: SVG logo of "文" U+6587 (1.9em, deep red, linked to "/") TARGET_LANGUAGE (.8rem, blue, underlined, linked to "/German", preserve upper/lower case) GAME: Compound Words (.8rem, dark grey) MUTE: Mute Button: The exact text "MUTE" (1rem). Toggles isMuted state, mutes all sound, text toggles to "UNMUTE". CRITICAL: You MUST use the exact localStorage key 'openlang_global_mute_state' to initialize isMuted on mount, and you MUST save the boolean value to this exact key every time the button is toggled so the setting persists universally across all games. INFO: Info icon: question mark ("?" 1.8rem): Opens Info modal. - Info Modal: - Explain rules in detail (point scoring for correct and penalty for wrong answer, streak bonus = length of the streak, hint rules, no points when hint is used, hint resets streak, MUTE button to kill sound, game is won when all cards are guessed). - Closing: Add a global `window.addEventListener('keydown')` to close modal when "Escape" is pressed or on any tap or mouse click. SCORE: "Score X" (.8rem) LEARNED: Learned Y% (.8rem) SOURCE: Button with the text "SOURCE" (1rem). Link . - Implementation: Use 'display: grid; grid-template-columns: 6% 24% 20% 50%; width: 100%; box-sizing: border-box; height: 2.5rem;', tight veritcal spacing. - Column 1: - LOGO - Column 2: - First Row: LANGUAGE - Second Row: GAME - Column 3: - First Row: SCORE - Second Row: LEARNED - Column 4: - Layout: This grid cell MUST be a flex container: 'display: flex; width: 100%; height: 100%; align-items: stretch; justify-content: flex-end; gap: 2px;'. - Button/Link Constraints: MUTE, INFO, SOURCE: - Every element in this column MUST have 'flex: 1 1 0%;' (force equal growth/shrink) and 'display: flex;'. - Use 'align-items: center; justify-content: center;' on the buttons so text remains centered as they grow. - Set 'width: 100%' and 'height: 100%' for each button/link to fill the header's vertical and horizontal space. - Appearance: Add a border (e.g., 'border: 1px solid #e5e7eb') and background to make them look like large, distinct touch targets. - General Styling: - Metadata Font: 10px to 12px. - Button Font: 9px to 11px, 'font-weight: bold', 'white-space: nowrap'. - Use 'text-center' on all text elements. 9. DEBUGGING & SAFETY (CRITICAL) --------------------------------- To prevent "Blank Screen" errors, you MUST implement a global error handler at the VERY TOP of the