0. CONTENT GENERATION --------------------- Start with this data: ------------------------------- Quote | Author I have a dream. | Martin Luther King Jr. We have no right to be tired. | Che Guevara Big Brother is watching you. | George Orwell Every state is founded on force. | Leon Trotsky I trust no one, not even myself. | Josef Stalin Fascism is capitalism in decay. | Vladimir Lenin Charity is a failure of government. | Zohran Mamdani Revolutions are the locomotives of history. | Karl Marx To achieve much, you must lose everything. | Che Guevara A winner is a dreamer who never gives up. | Nelson Mandela Freedom is the recognition of necessity. | Friedrich Engels A lie told often enough becomes the truth. | Vladimir Lenin Who controls the present controls the past. | George Orwell Silence is argument carried on by other means. | Che Guevara It always seems impossible until it’s done. | Nelson Mandela Death solves all problems; no man, no problem. | Josef Stalin Political power grows out of the barrel of a gun. | Mao Zedong An ounce of action is worth a ton of theory. | Friedrich Engels A riot is the language of the unheard. | Martin Luther King Jr. Better to die standing than to live on your knees. | Che Guevara It was patriotism, not communism, that inspired me. | Ho Chi Minh Those who do not move, do not notice their chains. | Rosa Luxemburg Nothing is more precious than independence and liberty. | Ho Chi Minh Condemn me. It does not matter. History will absolve me. | Fidel Castro One man with a gun can control one hundred without one. | Vladimir Lenin We think too small, like the frog at the bottom of the well. | Mao Zedong We are not broke, we are just unwilling to tax the rich. | Zohran Mamdani The true revolutionary is guided by a great feeling of love. | Che Guevara Liberty is precious; so precious that it must be rationed. | Vladimir Lenin Healthcare must be recognized as a right, not a privilege. | Bernie Sanders Housing is a human right, not a commodity for speculation. | Zohran Mamdani Hate cannot drive out hate; only love can do that. | Martin Luther King Jr. A single death is a tragedy; a million deaths is a statistic. | Josef Stalin Cruel leaders are replaced only to have new leaders turn cruel. | Che Guevara Men do not shape destiny, Destiny produces the man for the hour. | Fidel Castro Lead from the back — and let others believe they are in front. | Nelson Mandela Capitalists will sell us the rope with which we will hang them. | Vladimir Lenin Workers of the world unite; you have nothing to lose but your chains. | Karl Marx Freedom is always the freedom of the one who thinks differently. | Rosa Luxemburg In the Soviet army it takes more courage to retreat than to advance. | Josef Stalin Everything under heaven is in utter chaos; the situation is excellent. | Mao Zedong Let a hundred flowers bloom, let a hundred schools of thought contend. | Mao Zedong We don't just need a seat at the table; we need to flip the table. | Zohran Mamdani Education is a weapon whose effects depend on who holds it in his hands. | Josef Stalin A revolution is a struggle to the death between the future and the past. | Fidel Castro Free at last, Free at last, Thank God almighty we are free at last. | Martin Luther King Jr. Resentment is like drinking poison and then hoping it will kill your enemies. | Nelson Mandela The end may justify the means as long as there is something that justifies the end. | Leon Trotsky You can kill ten of my men for every one I kill... even at those odds, you will lose. | Ho Chi Minh Ideas are more powerful than guns. We would not let our enemies have guns, why ideas? | Josef Stalin Do not judge me by my successes, judge me by how many times I fell down and got back up. | Nelson Mandela The people who cast the votes decide nothing. The people who count the votes decide everything. | Josef Stalin ------------------------------- Use the entre quote, do not cut it in mid-sentence. Dataset Fields: id | text (English) | author | translation (_LANGUAGE Translation) Translation is of the English text only, not the author. Concatenate the author to the text (English) field: " –author". Do not start a new line. Do not add the author to the translation (_LANGUAGE) field. Delete the author column, and forget that the author field ever existed. Add an audio field: 1. GLOBAL CONFIGURATION ----------------------- - CARDS_PER_ROUND: 6 - BASE_LANGUAGE: English - TARGET_LANGUAGE: Phrases - 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_Phrases_Leftist_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. 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 TARGET_LANGUAGE translation using the calculated fixed height at .95rem. 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), 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: SVG logo of "文" U+6587 (1.9em, deep red, linked to "/") TARGET_LANGUAGE (.8rem, blue, underlined, linked to "/Phrases", preserve upper/lower case) GAME: Leftist (.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