0. CONTENT GENERATION --------------------- Generate a list of 40 short phrases and terms related to venture investment and startup culture in Ukraine's tech industry. 1. GLOBAL CONFIGURATION ----------------------- - CARDS_PER_ROUND: 10 - BASE_LANGUAGE: English - TARGET_LANGUAGE: Ukrainian - 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 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_Ukrainian_Invest_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, touch-action: none. (CRITICAL: Do NOT use overflow-y: auto, as touching the gaps between cards will trigger native column 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. Do NOT use manual timer calculations for Desktop mouse events. - Draggable Attribute: Conditionally set draggable={!isTouch}. Detect touch capability on mount. If the device is touch-enabled, draggable MUST be false. - 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: Apply touch-action: none; to the .game-card class. 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: MUST call `e.preventDefault()` to stop the whole column from natively scrolling/panning. 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. - No points awarded for this word if hint is used. - Set a global state `HintUsed` = true; - Strict: the match is not considered successful, the card that was double-clicked remains in play. - Reset streak. - Effect: Set a global state `hintTargetId`. The *TARGET_LANGUAGE* card with that ID flashes Orange for 2 seconds. The English card does NOT change color. The TARGET_LANGUAGE card is spoken. - SUCCESS: IDs match -> Flash Green 1 second -> TARGET_LANGUAGE text is spoken (if HintUsed = false) -> Cards Collapse/Slide Up 0. Streak bonus: equal to the length of the streak. 1. If !HintUsed, update Score (+10 + Streak Bonus). 2. If !HintUsed, add the matched ID to the list of `learnedIds` (merge with existing). 3. Save the new `score` and `streak`. 4. Persistence: On every successful match (handleMatch), immediately update `localStorage`. 5 Set a global state `HintUsed` = false; - FAIL: IDs mismatch -> Flash Red 2 sec -> Reset Streak -> Deduct Score (-5). 7. AUDIO SYSTEM (MOBILE ROBUSTNESS) ----------------------------------- Initialization Logic (Run inside Component): Detect Context: const isEnglishBrowser = navigator.language.toLowerCase().includes('en'); Voice State: Use useState to store available voices: const [voices, setVoices] = useState([]); Voice Loading (Mobile Fix): Use useEffect to listen to window.speechSynthesis.onvoiceschanged and update the voices state via setVoices(window.speechSynthesis.getVoices()). CRITICAL: On first user interaction (touchstart/mousedown), call cancel(), resume(), and speak a silent utterance to unlock the engine. Implementation (speakCard(item)): Mobile Queue Clear: Call window.speechSynthesis.cancel() and window.speechSynthesis.resume() immediately to prevent iOS/Android engine pausing. Configuration (The "isEnglishBrowser" Logic): IF isEnglishBrowser is TRUE: -> TargetLocale: Map LANGUAGE_CODE to standard (e.g., 'es' -> 'es-ES', 'fr' -> 'fr-FR'). -> TextSource: item.translation. ELSE: -> TargetLocale: 'en-US'. -> TextSource: item.text. Derive State (Dynamic Capability Detection): Calculate AUDIO_ENABLED dynamically based on the device's actual installed voices: const isLangInstalled = voices.some(voice => voice.lang.toLowerCase().startsWith(TargetLocale.split('-')[0].toLowerCase())); const AUDIO_ENABLED = isLangInstalled || RAW_DATA.some(item => item.audio !== null); Execution: Guard Clause: If !AUDIO_ENABLED, return immediately. a. Priority 1 (Hydrated): If item.audio exists, new Audio(item.audio).play(). b. Priority 2 (Synthesis Fallback): Assign a matching voice object from the voices array to utterance.voice and call speak() inside a 50ms setTimeout using the 'TextSource' and 'TargetLocale' determined in Configuration. 8. HEADER --------- - Header Elements LOGO: SVG logo of "文" U+6587 (1.9em, deep red, linked to "/") TARGET_LANGUAGE (.8rem, blue, underlined, linked to "/Ukrainian", preserve upper/lower case) GAME: VC Invest (.8rem, dark grey) MUTE: Mute Button: The exact text "MUTE" (1rem). Toggles `isMuted` state, mutes all sound, text toggles to "UNMUTE". 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