summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2025-03-07 13:36:46 +0100
committerYuval Adam <_@yuv.al>2025-03-07 13:36:46 +0100
commit0a9549db44f3adff71f6cc7249a1bcdb3db5a479 (patch)
treec7198365a31e16f0c84ad1328d641f8c80d91d88 /src
parenta3347ed1c24ef9182171cc461c02ea6bcbac734d (diff)
More fixes
Diffstat (limited to 'src')
-rw-r--r--src/components/OptionButtons.css102
-rw-r--r--src/pages/HomePage.tsx139
-rw-r--r--src/styles.css141
3 files changed, 239 insertions, 143 deletions
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<string | null>(null);
+ const [_correctAnswers, setCorrectAnswers] = useState<string[]>([]);
+ const [_incorrectAnswers, setIncorrectAnswers] = useState<string[]>([]);
const actxRef = useRef<any>(null);
const optionButtonsRef = useRef<Array<HTMLButtonElement | null>>([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 (
<div className="container">
<h1 onClick={resetGame} className="title-link">Morse Hero</h1>
@@ -319,11 +325,17 @@ const HomePage = () => {
{options.map((option, index) => (
<button
key={index}
- className="option-button"
+ className={`option-button ${selectedOption === option
+ ? option === currentChar
+ ? "correct"
+ : "incorrect"
+ : ""
+ }`}
ref={el => {
optionButtonsRef.current[index] = el;
}}
- onClick={() => checkAnswer(option)}
+ onClick={() => handleOptionClick(option, index)}
+ disabled={selectedOption !== null}
>
<span className="char-display">{option}</span>
{showHints && <span className="morse-hint">{getMorseCode(option)}</span>}
@@ -356,7 +368,6 @@ const HomePage = () => {
/>
<label htmlFor="hintMode">Show Hints</label>
</div>
- <button className="reset-button" onClick={resetGame}>New Game</button>
</div>
<div className="keyboard-tip">
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 */