0. CONTENT GENERATION --------------------- Create a dataset with these words: -------- ring rise risk robot roll romantic rope rough row royal rugby rule safety sail sailor sample sand scan scientific script sculpture secondary security seed sensible separate seriously servant set (put) set (group) setting sex sexual shake share sharp shelf shell shift shine shiny shoot shy sight signal silent silly similarity similarly simply since sink slice slightly slow smart smooth software soil solid sort southern specifically spending spicy spirit spoken spot spread spring stadium staff standard state statistic statue stick (push into/attach) stick (piece of wood) still store stranger strength string strongly studio stuff substance successfully sudden suffer suit suitable summarize summary supply supporter surely surface survive swim switch ------- Note for the translation below: If the word has no direct translation, briefly explain its use (max 1 sentence). 1. GLOBAL CONFIGURATION ----------------------- - CARDS_PER_ROUND: 9 - BASE_LANGUAGE: English - TARGET_LANGUAGE: Dutch - GAME: Intermediate-7 - LANGUAGE_CODE: Two letter standard code for TARGET_LANGUAGE (All Caps). - GA_MEASUREMENT_ID: "G-WFWF326Q9H" Dataset Fields: id | text (English) | translation (TARGET_LANGUAGE 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 include at the very top of the . - 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_' + CONFIG.TARGET_LANGUAGE + '_' + CONFIG.GAME + '_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. Write the total word count for cross-component access: `localStorage.setItem('openlang_' + CONFIG.TARGET_LANGUAGE + '_' + CONFIG.GAME + '_total', RAW_DATA.length);` 4. Filter `RAW_DATA` to exclude any IDs found in `learnedIds`. 5. Initialize `deckRef` using a Fisher-Yates shuffle of the *remaining* (unlearned) items. 6. If no items remain (all learned), trigger the Win Condition immediately. 7. Do NOT call startRound() here. Let the Round Transition useEffect handle loading the first batch automatically when it detects roundData.left.length === 0 on mount. - 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 (You MUST use .filter() to do this, never .splice()). When defining startRound(), ensure it reads the cards using .slice(0, CARDS_PER_ROUND) so the active cards remain safely inside deckRef until matched. - 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): Each card must be a single
element with key, data-id, data-side, className, style, and draggable all on the same element. Do not wrap cards in an outer container div. Left (English) Card: Display the English text using the calculated fixed height .95rem. Right (TARGET_LANGUAGE) Card: Display the translation. CRITICAL SIZING & LOCALIZATION: Do not force this side to .95rem. You MUST dynamically apply an appropriate font size based on the TARGET_LANGUAGE. For Arabic, Hebrew, Hindi, Thai, Chinese and Japanese use a larger font size (e.g., 1.5rem to 1.8rem / Tailwind text-2xl to text-3xl) so it is clearly legible and visually balanced with the English side. Additionally, if the TARGET_LANGUAGE is a Right-to-Left language (e.g., Arabic, Hebrew, Farsi, Urdu), you MUST include the dir="rtl" attribute on this HTML element. 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. FONT SUPPORT: Apply a robust modern system font fallback stack tailored to the TARGET_LANGUAGE to ensure non-Latin scripts render beautifully and readably without external font files. 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), update learnedIds, and immediately remove the item from deckRef.current. 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: An anchor tag wrapping the character "文" (U+6587). Style: font-size 1.9em, color deep red, no text-decoration. The entire element must be a clickable link to "/". TARGET_LANGUAGE (.8rem, blue, underlined, linked to "/Dutch", preserve upper/lower case) GAME: Intermediate-7 (.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