From 0a9549db44f3adff71f6cc7249a1bcdb3db5a479 Mon Sep 17 00:00:00 2001 From: Yuval Adam <_@yuv.al> Date: Fri, 7 Mar 2025 13:36:46 +0100 Subject: More fixes --- src/components/OptionButtons.css | 102 ++++++++++++++++++++++++++++ src/pages/HomePage.tsx | 139 ++++++++++++++++++++------------------ src/styles.css | 141 +++++++++++++++++---------------------- 3 files changed, 239 insertions(+), 143 deletions(-) create mode 100644 src/components/OptionButtons.css diff --git a/src/components/OptionButtons.css b/src/components/OptionButtons.css new file mode 100644 index 0000000..a513337 --- /dev/null +++ b/src/components/OptionButtons.css @@ -0,0 +1,102 @@ +.options { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 15px; + margin: 20px auto; + width: 100%; + max-width: 500px; + aspect-ratio: 2/1; +} + +.option-button { + padding: 20px; + font-size: 2rem; + font-weight: 600; + border: none; + border-radius: 8px; + background: rgba(30, 40, 50, 0.7); + color: #fff; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(5px); + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.option-button:hover { + transform: translateY(-3px); + background: rgba(40, 50, 60, 0.8); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.option-button:active { + transform: translateY(1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.option-button.correct { + background: #27ae60; + color: white; + animation: pulse 1s; +} + +.option-button.incorrect { + background: #e74c3c; + color: white; + animation: shake 0.5s; +} + +.char-display { + font-size: 38px; + font-weight: bold; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); +} + +.morse-hint { + display: block; + font-size: 18px; + margin-top: 10px; + color: rgba(255, 255, 255, 0.7); + font-family: monospace; + letter-spacing: 2px; +} + +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + 10%, 30%, 50%, 70%, 90% { + transform: translateX(-5px); + } + 20%, 40%, 60%, 80% { + transform: translateX(5px); + } +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(39, 174, 96, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(39, 174, 96, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(39, 174, 96, 0); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 442d8c4..f0e1732 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -2,11 +2,14 @@ import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router'; import cw from 'cw'; import '../styles.css'; +import '../components/OptionButtons.css'; import Footer from '../components/Footer'; declare global { interface Window { cw: any; + AudioContext: typeof AudioContext; + webkitAudioContext: typeof AudioContext; } } @@ -20,6 +23,9 @@ const HomePage = () => { const [wpm, setWpm] = useState(20); const [showHints, setShowHints] = useState(false); const [showHowToPlay, setShowHowToPlay] = useState(false); + const [selectedOption, setSelectedOption] = useState(null); + const [_correctAnswers, setCorrectAnswers] = useState([]); + const [_incorrectAnswers, setIncorrectAnswers] = useState([]); const actxRef = useRef(null); const optionButtonsRef = useRef>([null, null, null, null]); @@ -45,6 +51,7 @@ const HomePage = () => { // Generate a new round const newRound = () => { + setSelectedOption(null); // Reset button states optionButtonsRef.current.forEach(button => { if (button) { @@ -59,18 +66,15 @@ const HomePage = () => { setCurrentChar(newChar); // Generate options (one correct, three random) - let newOptions = [newChar]; - - // Add three random unique characters - while (newOptions.length < 4) { - const randomChar = allChars[Math.floor(Math.random() * allChars.length)]; - if (!newOptions.includes(randomChar)) { - newOptions.push(randomChar); - } - } + const generateOptions = (correctChar: string): string[] => { + const possibleChars = allChars.split(""); + const filteredChars = possibleChars.filter(char => char !== correctChar); + const shuffled = [...filteredChars].sort(() => 0.5 - Math.random()); + const options = [correctChar, ...shuffled.slice(0, 3)]; + return options.sort(() => 0.5 - Math.random()); + }; - // Shuffle options - newOptions = shuffleArray(newOptions); + const newOptions = generateOptions(newChar); setOptions(newOptions); // Play the Morse code for the character @@ -79,18 +83,28 @@ const HomePage = () => { // Play Morse code for a character const playMorseCode = (char: string) => { - if (actxRef.current) { + if (!char) return; + + try { + if (!actxRef.current) { + actxRef.current = cw.initAudioContext({ tone: 600 }); + } + cw.play(char, { wpm: wpm, actx: actxRef.current }); + } catch (error) { + console.error('Error playing Morse code:', error); } }; // Check if the answer is correct const checkAnswer = (selectedChar: string) => { - if (selectedChar === currentChar) { - // Correct answer + setSelectedOption(selectedChar); + const isCorrect = selectedChar === currentChar; + + if (isCorrect) { setScore(prevScore => prevScore + 1); // Add to score tracker @@ -100,24 +114,7 @@ const HomePage = () => { marker.className = 'score-marker correct'; scoreTracker.appendChild(marker); } - - // Disable all buttons - optionButtonsRef.current.forEach(button => { - if (button) button.disabled = true; - }); - - // Highlight the correct button - const correctButtonIndex = options.indexOf(currentChar); - if (correctButtonIndex !== -1 && optionButtonsRef.current[correctButtonIndex]) { - const button = optionButtonsRef.current[correctButtonIndex]; - if (button) button.classList.add('correct'); - } - - // Move to next round after delay - setTimeout(newRound, 1500); } else { - // Incorrect answer - // Add to score tracker const scoreTracker = document.getElementById('scoreTracker'); if (scoreTracker) { @@ -126,29 +123,25 @@ const HomePage = () => { scoreTracker.appendChild(marker); } - // Disable all buttons - optionButtonsRef.current.forEach(button => { - if (button) button.disabled = true; - }); - - // Highlight the incorrect button and show the correct one - const selectedButtonIndex = options.indexOf(selectedChar); - if (selectedButtonIndex !== -1 && optionButtonsRef.current[selectedButtonIndex]) { - const button = optionButtonsRef.current[selectedButtonIndex]; - if (button) button.classList.add('incorrect'); - } - - const correctButtonIndex = options.indexOf(currentChar); - if (correctButtonIndex !== -1 && optionButtonsRef.current[correctButtonIndex]) { - const button = optionButtonsRef.current[correctButtonIndex]; - if (button) button.classList.add('correct'); + // Highlight the correct answer + const correctIndex = options.findIndex(option => option === currentChar); + if (correctIndex !== -1) { + const correctButton = optionButtonsRef.current[correctIndex]; + if (correctButton) { + // Add a small delay before showing the correct answer + setTimeout(() => { + correctButton.classList.add('correct'); + }, 300); + } } - - // Move to next round after delay - setTimeout(newRound, 2000); } setTotalPlayed(prevTotal => prevTotal + 1); + + // Proceed to next round after a delay + setTimeout(() => { + newRound(); + }, 1500); // Increased delay to give time to see the correct answer }; // Handle keyboard input @@ -166,7 +159,7 @@ const HomePage = () => { button.classList.add('key-pressed'); setTimeout(() => { button.classList.remove('key-pressed'); - checkAnswer(key); + handleOptionClick(key, index); }, 100); } } @@ -178,16 +171,6 @@ const HomePage = () => { }; }, [gameStarted, options]); - // Utility function to shuffle an array - const shuffleArray = (array: string[]) => { - const newArray = [...array]; - for (let i = newArray.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; - } - return newArray; - }; - // Get Morse code representation for a character const getMorseCode = (char: string) => { const morseMap: { [key: string]: string } = { @@ -211,6 +194,8 @@ const HomePage = () => { setTotalPlayed(0); setCurrentChar(''); setOptions([]); + setCorrectAnswers([]); + setIncorrectAnswers([]); // Clear score tracker const scoreTracker = document.getElementById('scoreTracker'); @@ -229,6 +214,27 @@ const HomePage = () => { } }; + // Handle option button click + const handleOptionClick = (option: string, index: number) => { + const button = optionButtonsRef.current[index]; + if (button) { + // Disable all buttons to prevent multiple selections + optionButtonsRef.current.forEach(btn => { + if (btn) btn.disabled = true; + }); + + // Add appropriate class based on correctness + if (option === currentChar) { + button.classList.add('correct'); + } else { + button.classList.add('incorrect'); + } + + // Check the answer + checkAnswer(option); + } + }; + return (

Morse Hero

@@ -319,11 +325,17 @@ const HomePage = () => { {options.map((option, index) => (
-
diff --git a/src/styles.css b/src/styles.css index 87cc81b..33213a8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -26,6 +26,7 @@ --header-height: 70px; --footer-height: 100px; --glow-color: rgba(243, 156, 18, 0.6); + --error-color: #e74c3c; } * { @@ -301,82 +302,33 @@ h1::after { .options { display: grid; grid-template-columns: 1fr 1fr; - gap: 30px; - margin-top: 40px; + grid-template-rows: 1fr 1fr; + gap: 15px; + margin: 20px auto; + width: 100%; + max-width: 500px; + aspect-ratio: 2/1; } -.start-button { - background: linear-gradient(145deg, var(--accent-color) 0%, #c0392b 100%); - color: white; +.option-button { + padding: 20px; + font-size: 2rem; + font-weight: 600; border: none; - padding: 20px 50px; - font-size: 32px; - font-weight: 700; - border-radius: var(--border-radius-lg); + border-radius: var(--border-radius-md); + background: var(--card-bg); + color: var(--text-light); cursor: pointer; transition: all var(--transition-normal); box-shadow: var(--shadow-md); - text-transform: uppercase; - letter-spacing: 1px; - margin-bottom: 30px; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(5px); width: 100%; - max-width: 300px; - position: relative; - z-index: 1; -} - -.start-button:hover { - transform: translateY(-5px); - box-shadow: var(--shadow-lg); - background: linear-gradient(145deg, #e74c3c 0%, var(--accent-color) 100%); -} - -.start-button:active { - transform: translateY(-2px); - box-shadow: var(--shadow-sm); -} - -.option-button { - background: linear-gradient(145deg, var(--primary-color), var(--primary-dark)); - color: white; - border-radius: var(--border-radius-md); - font-size: 36px; - padding: 25px 15px; - box-shadow: 0 8px 0 var(--primary-dark), - var(--shadow-md); - min-height: 130px; + height: 100%; display: flex; - flex-direction: column; align-items: center; justify-content: center; - position: relative; - z-index: 1; -} - -.option-button:hover { - transform: translateY(-5px); - box-shadow: 0 13px 0 var(--primary-dark), - var(--shadow-lg); -} - -.option-button:active { - transform: translateY(3px); - box-shadow: 0 5px 0 var(--primary-dark), - var(--shadow-sm); -} - -.option-button.correct { - background: linear-gradient(145deg, var(--success-color), var(--success-dark)); - box-shadow: 0 8px 0 var(--success-dark), - 0 0 30px rgba(46, 204, 113, 0.5); - animation: pulse 0.5s; -} - -.option-button.incorrect { - background: linear-gradient(145deg, var(--accent-color), var(--accent-dark)); - box-shadow: 0 8px 0 var(--accent-dark), - 0 0 30px rgba(231, 76, 60, 0.5); - animation: shake 0.5s; + flex-direction: column; } .char-display { @@ -513,6 +465,11 @@ h1::after { border-color: rgba(52, 152, 219, 0.5); } +#startScreen .chart-link a:active { + transform: translateY(1px); + box-shadow: var(--shadow-sm); +} + .score-container { margin-bottom: 20px; width: 100%; @@ -572,7 +529,7 @@ h1::after { } .score-marker.incorrect { - background-color: var(--accent-color); + background-color: var(--error-color); box-shadow: 0 0 5px rgba(231, 76, 60, 0.7); } @@ -590,7 +547,7 @@ h1::after { } .score-dot.incorrect { - background-color: var(--accent-color); + background-color: var(--error-color); box-shadow: 0 0 5px rgba(231, 76, 60, 0.7); } @@ -1065,11 +1022,6 @@ body.game-active #startScreen { animation: correct-pulse 1s infinite; } -.option-button.incorrect { - background: linear-gradient(145deg, var(--accent-color), var(--accent-dark)); - animation: incorrect-shake 0.5s; -} - /* Start screen styles */ #startScreen { max-width: 600px; @@ -1422,18 +1374,49 @@ body.game-active #startScreen { } .replay-button { - background: linear-gradient(145deg, var(--primary-color), var(--primary-dark)); - color: white; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: var(--text-light); padding: 8px 15px; - border-radius: var(--border-radius-sm); + border-radius: var(--border-radius-md); font-size: 0.9rem; - display: inline-flex; + cursor: pointer; + transition: all var(--transition-normal); + display: flex; align-items: center; - gap: 8px; + justify-content: center; + gap: 5px; + margin: 15px auto; + backdrop-filter: blur(5px); +} + +.replay-button:hover { + background: rgba(255, 255, 255, 0.15); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.replay-button:active { + transform: translateY(1px); + box-shadow: var(--shadow-sm); } .replay-icon { - font-size: 1.2rem; + font-size: 1.1rem; + animation: spin 2s linear infinite paused; +} + +.replay-button:hover .replay-icon { + animation-play-state: running; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } /* Settings styles */ -- cgit v1.3.1