Created
March 14, 2026 12:39
-
-
Save nsdevaraj/64d6a03695d10bb5cc404fe2ab42a81c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Morse Code Tutor + Trainer</title> | |
| <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', sans-serif; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useRef } = React; | |
| const MORSE = { | |
| A: ".-", B: "-...", C: "-.-.", D: "-..", E: ".", F: "..-.", G: "--.", H: "....", | |
| I: "..", J: ".---", K: "-.-", L: ".-..", M: "--", N: "-.", O: "---", P: ".--.", | |
| Q: "--.-", R: ".-.", S: "...", T: "-", U: "..-", V: "...-", W: ".--", X: "-..-", | |
| Y: "-.--", Z: "--.." | |
| }; | |
| const N_ = { | |
| START: [460, 32], | |
| T: [340, 72], E: [560, 72], | |
| M: [200, 72], I: [680, 72], | |
| O: [68, 72], S: [790, 72], H: [905, 72], | |
| G: [228, 205], U: [610, 205], | |
| Q: [78, 205], V: [755, 205], | |
| Z: [268, 335], F: [648, 335], | |
| N: [368, 455], A: [530, 455], K: [208, 455], R: [680, 455], | |
| Y: [68, 455], L: [832, 455], | |
| C: [230, 555], | |
| D: [388, 645], | |
| X: [158, 735], W: [548, 735], P: [710, 735], | |
| B: [318, 835], J: [588, 835], | |
| }; | |
| const EDGES = [ | |
| ["START", "T", "dash"], ["START", "E", "dot"], | |
| ["T", "M", "dash"], ["M", "O", "dash"], | |
| ["E", "I", "dot"], ["I", "S", "dot"], ["S", "H", "dot"], | |
| ["M", "G", "dot"], ["G", "Q", "dash"], ["G", "Z", "dot"], | |
| ["I", "U", "dash"], ["U", "F", "dot"], ["S", "V", "dash"], | |
| ["T", "N", "dot"], ["E", "A", "dash"], | |
| ["N", "K", "dash"], ["K", "Y", "dash"], ["K", "C", "dot"], | |
| ["A", "R", "dot"], ["R", "L", "dot"], | |
| ["N", "D", "dot"], ["D", "X", "dash"], ["D", "B", "dot"], | |
| ["A", "W", "dash"], ["W", "P", "dot"], ["W", "J", "dash"], | |
| ]; | |
| const childMap = {}; | |
| EDGES.forEach(([p, c, t]) => { | |
| if (!childMap[p]) childMap[p] = {}; | |
| childMap[p][t] = c; | |
| }); | |
| function getPath(letter) { | |
| const morse = MORSE[letter]; | |
| if (!morse) return []; | |
| const path = ["START"]; | |
| let cur = "START"; | |
| for (const s of morse) { | |
| const type = s === "." ? "dot" : "dash"; | |
| const next = childMap[cur]?.[type]; | |
| if (!next) break; | |
| path.push(next); | |
| cur = next; | |
| } | |
| return path; | |
| } | |
| function getActiveEdges(path) { | |
| const set = new Set(); | |
| for (let i = 0; i < path.length - 1; i++) { | |
| set.add(`${path[i]}-${path[i + 1]}`); | |
| } | |
| return set; | |
| } | |
| function midXY(a, b) { | |
| return [(N_[a][0] + N_[b][0]) / 2, (N_[a][1] + N_[b][1]) / 2]; | |
| } | |
| const Rod = ({ x1, y1, x2, y2, glow }) => ( | |
| <line x1={x1} y1={y1} x2={x2} y2={y2} | |
| stroke={glow ? "#b45309" : "#3a3a3a"} | |
| strokeWidth={glow ? 5 : 4} | |
| strokeLinecap="round" | |
| /> | |
| ); | |
| const DotShape = ({ cx, cy, glow, vertical }) => ( | |
| <g> | |
| {glow && <circle cx={cx} cy={cy} r={16} fill="#f59e0b" opacity={0.2} />} | |
| <circle cx={cx} cy={cy} r={11} | |
| fill={glow ? "url(#dotGlow)" : "url(#dotDark)"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5} /> | |
| <ellipse cx={cx - 3} cy={cy - 3} rx={4} ry={3} fill="rgba(255,255,255,0.12)" /> | |
| </g> | |
| ); | |
| const DashShape = ({ cx, cy, glow, vertical }) => { | |
| const w = vertical ? 18 : 44; | |
| const h = vertical ? 44 : 18; | |
| return ( | |
| <g> | |
| {glow && <rect x={cx - w / 2 - 3} y={cy - h / 2 - 3} width={w + 6} height={h + 6} rx={6} fill="#f59e0b" opacity={0.18} />} | |
| <rect x={cx - w / 2} y={cy - h / 2} width={w} height={h} rx={4} | |
| fill={glow ? "url(#dashGlow)" : "url(#dashDark)"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5} /> | |
| <rect x={cx - w / 2 + 3} y={cy - h / 2 + 3} width={vertical ? 12 : 26} height={vertical ? 12 : 5} rx={2} fill="rgba(255,255,255,0.08)" /> | |
| </g> | |
| ); | |
| }; | |
| const NodeCircle = ({ cx, cy, glow, label, labelPos }) => { | |
| const off = labelPos || "below"; | |
| let lx = cx, ly = cy; | |
| if (off === "below") { ly = cy + 22; } | |
| else if (off === "above") { ly = cy - 16; } | |
| else if (off === "left") { lx = cx - 18; ly = cy + 2; } | |
| else if (off === "right") { lx = cx + 18; ly = cy + 2; } | |
| return ( | |
| <g> | |
| {glow && <circle cx={cx} cy={cy} r={14} fill="#f59e0b" opacity={0.25} />} | |
| <circle cx={cx} cy={cy} r={8} | |
| fill={glow ? "#92400e" : "#222"} | |
| stroke={glow ? "#fbbf24" : "#555"} strokeWidth={1.5} /> | |
| {glow && <circle cx={cx} cy={cy} r={4} fill="#fde68a" opacity={0.6} />} | |
| <text x={lx} y={ly} textAnchor="middle" dominantBaseline="central" | |
| fontSize={16} fontWeight={800} fontFamily="'Courier New',monospace" | |
| fill={glow ? "#fde68a" : "#777"} | |
| style={glow ? { filter: "url(#textGlow)" } : {}}> | |
| {label} | |
| </text> | |
| </g> | |
| ); | |
| }; | |
| const labelPositions = { | |
| O: "above", M: "above", T: "above", E: "above", I: "above", S: "above", H: "above", | |
| Q: "left", G: "right", U: "left", V: "right", | |
| Z: "right", F: "right", | |
| Y: "above", K: "above", N: "above", A: "above", R: "above", L: "above", | |
| C: "right", D: "right", X: "left", W: "left", P: "right", | |
| B: "right", J: "right" | |
| }; | |
| function MorseTutor() { | |
| const [text, setText] = useState(""); | |
| const [charIdx, setCharIdx] = useState(0); | |
| const [playing, setPlaying] = useState(false); | |
| const [playIdx, setPlayIdx] = useState(-1); | |
| const [mode, setMode] = useState("visual"); | |
| const [wpm, setWpm] = useState(15); | |
| const [quizScore, setQuizScore] = useState(0); | |
| const [quizTotal, setQuizTotal] = useState(0); | |
| const [currentLetter, setCurrentLetter] = useState(""); | |
| const [userInput, setUserInput] = useState(""); | |
| const [showFeedback, setShowFeedback] = useState(false); | |
| const [isCorrect, setIsCorrect] = useState(false); | |
| const [isPlayingQuiz, setIsPlayingQuiz] = useState(false); | |
| const stopRef = useRef(false); | |
| const audioCtx = useRef(null); | |
| const chars = text.toUpperCase().split("").filter(c => MORSE[c] || c === " "); | |
| const currentChar = playing ? (chars[playIdx] || "") : (chars[charIdx] || ""); | |
| const path = currentChar && currentChar !== " " ? getPath(currentChar) : []; | |
| const activeNodes = new Set(path); | |
| const activeEdges = getActiveEdges(path); | |
| const getAudioCtx = () => { | |
| if (!audioCtx.current) audioCtx.current = new (window.AudioContext || window.webkitAudioContext)(); | |
| return audioCtx.current; | |
| }; | |
| const beep = (dur) => new Promise(res => { | |
| const ctx = getAudioCtx(); | |
| const o = ctx.createOscillator(); | |
| const g = ctx.createGain(); | |
| o.frequency.value = 620; o.type = "sine"; g.gain.value = 0.12; | |
| o.connect(g); g.connect(ctx.destination); | |
| o.start(); | |
| setTimeout(() => { o.stop(); res(); }, dur); | |
| }); | |
| const sleep = ms => new Promise(r => setTimeout(r, ms)); | |
| const playMorse = async () => { | |
| if (playing) { stopRef.current = true; return; } | |
| if (chars.length === 0) return; | |
| stopRef.current = false; | |
| setPlaying(true); | |
| for (let i = 0; i < chars.length; i++) { | |
| if (stopRef.current) break; | |
| setPlayIdx(i); | |
| const c = chars[i]; | |
| const m = MORSE[c]; | |
| if (!m) { await sleep(400); continue; } | |
| for (const s of m) { | |
| if (stopRef.current) break; | |
| await beep(s === "." ? 80 : 240); | |
| await sleep(80); | |
| } | |
| await sleep(350); | |
| } | |
| setPlayIdx(-1); | |
| setPlaying(false); | |
| }; | |
| const handleInput = (e) => { | |
| setText(e.target.value); | |
| setCharIdx(0); | |
| }; | |
| const morseStr = currentChar && MORSE[currentChar] ? MORSE[currentChar] : ""; | |
| const btnStyle = (bg, color) => ({ | |
| background: bg, border: "1px solid #444", borderRadius: 6, padding: "6px 16px", | |
| color, fontSize: 12, fontWeight: 700, cursor: "pointer", letterSpacing: 1, | |
| textTransform: "uppercase", fontFamily: "'Courier New',monospace", | |
| }); | |
| // === TRAINER HELPERS === | |
| const getRandomLetter = () => Object.keys(MORSE)[Math.floor(Math.random() * Object.keys(MORSE).length)]; | |
| const calculateTimings = (speed) => { | |
| const dit = Math.max(20, 1200 / speed); | |
| return { dot: dit, dash: 3 * dit, intra: dit, letterSpace: 3 * dit }; | |
| }; | |
| const playSingleLetter = async (letter, speed) => { | |
| if (isPlayingQuiz) return; | |
| setIsPlayingQuiz(true); | |
| const timings = calculateTimings(speed); | |
| const m = MORSE[letter]; | |
| if (!m) { | |
| setIsPlayingQuiz(false); | |
| return; | |
| } | |
| for (const s of m) { | |
| await beep(s === "." ? timings.dot : timings.dash); | |
| await sleep(timings.intra); | |
| } | |
| await sleep(timings.letterSpace); | |
| setIsPlayingQuiz(false); | |
| }; | |
| const nextQuiz = () => { | |
| const letter = getRandomLetter(); | |
| setCurrentLetter(letter); | |
| setUserInput(""); | |
| setShowFeedback(false); | |
| setIsCorrect(false); | |
| playSingleLetter(letter, wpm); | |
| }; | |
| const startQuiz = () => { | |
| setQuizScore(0); | |
| setQuizTotal(0); | |
| setCurrentLetter(""); | |
| nextQuiz(); | |
| }; | |
| const submitGuess = () => { | |
| if (!currentLetter || userInput.trim() === "") return; | |
| const guess = userInput.trim().toUpperCase(); | |
| const correct = guess === currentLetter; | |
| setIsCorrect(correct); | |
| setShowFeedback(true); | |
| setQuizScore(prev => prev + (correct ? 1 : 0)); | |
| setQuizTotal(prev => prev + 1); | |
| setTimeout(() => { | |
| nextQuiz(); | |
| }, 1200); | |
| }; | |
| const endQuiz = () => { | |
| setCurrentLetter(""); | |
| setUserInput(""); | |
| setShowFeedback(false); | |
| }; | |
| return ( | |
| <div style={{ | |
| minHeight: "100vh", | |
| background: "linear-gradient(160deg,#0a0a0a 0%,#151515 40%,#0e0e0e 100%)", | |
| color: "#ccc", fontFamily: "'Segoe UI',sans-serif", | |
| padding: "16px 8px", boxSizing: "border-box", | |
| }}> | |
| <div style={{ maxWidth: 720, margin: "0 auto" }}> | |
| <div style={{ textAlign: "center", marginBottom: 12 }}> | |
| <h1 style={{ fontSize: 20, fontWeight: 800, color: "#ddd", margin: 0, letterSpacing: 6, fontFamily: "'Courier New',monospace", textTransform: "uppercase" }}> | |
| Morse Code Tutor | |
| </h1> | |
| <div style={{ width: 50, height: 2, margin: "6px auto", background: "linear-gradient(90deg,transparent,#f59e0b,transparent)" }} /> | |
| </div> | |
| {/* TABS */} | |
| <div style={{ display: "flex", justifyContent: "center", gap: 8, marginBottom: 16 }}> | |
| <button | |
| onClick={() => setMode("visual")} | |
| style={btnStyle(mode === "visual" ? "#78350f" : "#1a1a1a", mode === "visual" ? "#fde68a" : "#888")}> | |
| Visual Tutor | |
| </button> | |
| <button | |
| onClick={() => setMode("trainer")} | |
| style={btnStyle(mode === "trainer" ? "#78350f" : "#1a1a1a", mode === "trainer" ? "#fde68a" : "#888")}> | |
| Trainer (Speed + Quiz) | |
| </button> | |
| </div> | |
| {/* VISUAL MODE */} | |
| {mode === "visual" && ( | |
| <div style={{ background: "#111", borderRadius: 12, border: "1px solid #252525", padding: "8px", marginBottom: 10, boxShadow: "inset 0 4px 20px rgba(0,0,0,0.7)" }}> | |
| <svg viewBox="0 0 960 890" width="100%" style={{ display: "block" }}> | |
| <defs> | |
| <radialGradient id="dotGlow" cx="50%" cy="40%" r="60%"> | |
| <stop offset="0%" stopColor="#fde68a" /> | |
| <stop offset="60%" stopColor="#f59e0b" /> | |
| <stop offset="100%" stopColor="#92400e" /> | |
| </radialGradient> | |
| <radialGradient id="dotDark" cx="35%" cy="35%" r="65%"> | |
| <stop offset="0%" stopColor="#444" /> | |
| <stop offset="100%" stopColor="#1a1a1a" /> | |
| </radialGradient> | |
| <linearGradient id="dashGlow" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="#fde68a" /> | |
| <stop offset="50%" stopColor="#f59e0b" /> | |
| <stop offset="100%" stopColor="#92400e" /> | |
| </linearGradient> | |
| <linearGradient id="dashDark" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stopColor="#444" /> | |
| <stop offset="50%" stopColor="#2a2a2a" /> | |
| <stop offset="100%" stopColor="#1a1a1a" /> | |
| </linearGradient> | |
| <filter id="textGlow"> | |
| <feGaussianBlur stdDeviation="3" result="blur" /> | |
| <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge> | |
| </filter> | |
| <filter id="lineGlow"> | |
| <feGaussianBlur stdDeviation="4" result="blur" /> | |
| <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge> | |
| </filter> | |
| </defs> | |
| <rect width="960" height="890" fill="#0f0f0f" rx="8" /> | |
| {EDGES.map(([p, c, t], i) => { | |
| const [x1, y1] = N_[p]; | |
| const [x2, y2] = N_[c]; | |
| const key = `${p}-${c}`; | |
| const glow = activeEdges.has(key); | |
| return ( | |
| <g key={i} style={glow ? { filter: "url(#lineGlow)" } : {}}> | |
| <Rod x1={x1} y1={y1} x2={x2} y2={y2} glow={glow} /> | |
| </g> | |
| ); | |
| })} | |
| {EDGES.map(([p, c, t], i) => { | |
| const [mx, my] = midXY(p, c); | |
| const key = `${p}-${c}`; | |
| const glow = activeEdges.has(key); | |
| const vertical = Math.abs(N_[p][1] - N_[c][1]) > Math.abs(N_[p][0] - N_[c][0]); | |
| if (t === "dot") return <DotShape key={`s${i}`} cx={mx} cy={my} glow={glow} vertical={vertical} />; | |
| return <DashShape key={`s${i}`} cx={mx} cy={my} glow={glow} vertical={vertical} />; | |
| })} | |
| {(() => { | |
| const [sx, sy] = N_.START; | |
| const glow = activeNodes.has("START"); | |
| return ( | |
| <g> | |
| <polygon points={`${sx - 12},${sy - 16} ${sx + 12},${sy - 16} ${sx},${sy + 4}`} | |
| fill={glow ? "#92400e" : "#2a2a2a"} stroke={glow ? "#fbbf24" : "#555"} strokeWidth={2} /> | |
| {glow && <polygon points={`${sx - 12},${sy - 16} ${sx + 12},${sy - 16} ${sx},${sy + 4}`} | |
| fill="#f59e0b" opacity={0.3} />} | |
| </g> | |
| ); | |
| })()} | |
| {Object.entries(N_).filter(([k]) => k !== "START").map(([letter, [cx, cy]]) => ( | |
| <NodeCircle | |
| key={letter} | |
| cx={cx} | |
| cy={cy} | |
| glow={activeNodes.has(letter)} | |
| label={letter} | |
| labelPos={labelPositions[letter]} | |
| /> | |
| ))} | |
| </svg> | |
| <div style={{ textAlign: "center", marginBottom: 8, minHeight: 50 }}> | |
| {currentChar && currentChar !== " " && ( | |
| <div style={{ display: "inline-flex", alignItems: "center", gap: 12, background: "rgba(245,158,11,0.08)", border: "1px solid rgba(245,158,11,0.2)", borderRadius: 10, padding: "8px 20px" }}> | |
| <span style={{ fontSize: 28, fontWeight: 900, fontFamily: "'Courier New',monospace", color: "#fde68a" }}>{currentChar}</span> | |
| <span style={{ fontSize: 20, fontFamily: "monospace", color: "#f59e0b", letterSpacing: 4 }}>{morseStr}</span> | |
| <span style={{ fontSize: 13, color: "#888", fontFamily: "monospace" }}> | |
| {morseStr.split("").map(s => s === "." ? "dot" : "dash").join(" β ")} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| <div style={{ background: "#131313", borderRadius: 10, border: "1px solid #222", padding: 12 }}> | |
| <input type="text" value={text} onChange={handleInput} placeholder="Type a message..." | |
| maxLength={40} | |
| style={{ | |
| width: "100%", background: "#0a0a0a", border: "1px solid #333", borderRadius: 8, | |
| padding: "10px 14px", fontSize: 18, fontFamily: "'Courier New',monospace", | |
| color: "#fde68a", letterSpacing: 3, outline: "none", boxSizing: "border-box", | |
| caretColor: "#f59e0b", | |
| }} /> | |
| <div style={{ display: "flex", gap: 8, marginTop: 10, justifyContent: "center", flexWrap: "wrap", alignItems: "center" }}> | |
| {chars.length > 1 && !playing && ( | |
| <> | |
| <button onClick={() => setCharIdx(Math.max(0, charIdx - 1))} style={btnStyle("#1a1a1a", "#666")}>Prev</button> | |
| <span style={{ color: "#888", fontSize: 13, fontFamily: "monospace", minWidth: 60, textAlign: "center" }}> | |
| {charIdx + 1} / {chars.length} | |
| </span> | |
| <button onClick={() => setCharIdx(Math.min(chars.length - 1, charIdx + 1))} style={btnStyle("#1a1a1a", "#666")}>Next</button> | |
| <div style={{ width: 1, height: 24, background: "#333", margin: "0 4px" }} /> | |
| </> | |
| )} | |
| <button onClick={playMorse} style={btnStyle(playing ? "#7f1d1d" : "#78350f", playing ? "#fca5a5" : "#fde68a")}> | |
| {playing ? "Stop" : "Transmit"} | |
| </button> | |
| <button onClick={() => { setText(""); setCharIdx(0); }} style={btnStyle("#1a1a1a", "#888")}>Clear</button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* TRAINER MODE - NEW FEATURE */} | |
| {mode === "trainer" && ( | |
| <div style={{ background: "#111", borderRadius: 12, border: "1px solid #252525", padding: "20px", boxShadow: "inset 0 4px 20px rgba(0,0,0,0.7)" }}> | |
| <h2 style={{ fontSize: 18, color: "#f59e0b", textAlign: "center", marginBottom: 16, fontFamily: "'Courier New',monospace" }}>TRAINER MODE</h2> | |
| {/* SPEED SETTINGS */} | |
| <div style={{ textAlign: "center", marginBottom: 24 }}> | |
| <label style={{ display: "block", marginBottom: 8, color: "#ccc", fontSize: 15 }}> | |
| Speed: <strong>{wpm} WPM</strong> (dit = {Math.round(1200 / wpm)} ms) | |
| </label> | |
| <input | |
| type="range" | |
| min="5" | |
| max="40" | |
| step="1" | |
| value={wpm} | |
| onChange={(e) => setWpm(parseInt(e.target.value))} | |
| style={{ width: "70%", accentColor: "#f59e0b" }} | |
| /> | |
| <div style={{ display: "flex", justifyContent: "center", gap: 8, marginTop: 12 }}> | |
| <button onClick={() => setWpm(8)} style={btnStyle("#1a1a1a", "#888")}>Slow 8</button> | |
| <button onClick={() => setWpm(15)} style={btnStyle("#1a1a1a", "#888")}>Medium 15</button> | |
| <button onClick={() => setWpm(25)} style={btnStyle("#1a1a1a", "#888")}>Fast 25</button> | |
| </div> | |
| </div> | |
| {/* QUIZ CONTROLS */} | |
| <div style={{ display: "flex", justifyContent: "center", gap: 12, marginBottom: 20 }}> | |
| <button onClick={startQuiz} style={btnStyle("#78350f", "#fde68a")}>START NEW QUIZ</button> | |
| {quizTotal > 0 && <button onClick={endQuiz} style={btnStyle("#1a1a1a", "#888")}>END SESSION</button>} | |
| </div> | |
| {quizTotal > 0 && ( | |
| <div style={{ textAlign: "center", fontSize: 18, marginBottom: 20, color: "#fde68a" }}> | |
| SCORE: <strong>{quizScore}</strong> / {quizTotal} ({quizTotal > 0 ? Math.round((quizScore / quizTotal) * 100) : 0}%) | |
| </div> | |
| )} | |
| {currentLetter ? ( | |
| <div style={{ background: "#131313", borderRadius: 12, padding: 24, textAlign: "center" }}> | |
| <p style={{ marginBottom: 16, color: "#ccc", fontSize: 15 }}>Listen & type the letter:</p> | |
| <button | |
| onClick={() => playSingleLetter(currentLetter, wpm)} | |
| disabled={isPlayingQuiz} | |
| style={btnStyle(isPlayingQuiz ? "#4a2a0f" : "#78350f", "#fde68a")}> | |
| {isPlayingQuiz ? "Playing..." : "π Replay"} | |
| </button> | |
| <div style={{ margin: "20px 0" }}> | |
| <input | |
| type="text" | |
| value={userInput} | |
| onChange={(e) => setUserInput(e.target.value.toUpperCase().slice(0, 1))} | |
| maxLength={1} | |
| onKeyPress={(e) => e.key === "Enter" && submitGuess()} | |
| style={{ | |
| width: 90, textAlign: "center", fontSize: 42, | |
| fontFamily: "'Courier New',monospace", | |
| background: "#0a0a0a", border: "3px solid #f59e0b", | |
| color: "#fde68a", borderRadius: 12, padding: 12, | |
| }} | |
| placeholder="?" | |
| /> | |
| </div> | |
| <button onClick={submitGuess} style={btnStyle("#78350f", "#fde68a")}>SUBMIT GUESS</button> | |
| {showFeedback && ( | |
| <div style={{ | |
| marginTop: 20, | |
| padding: 14, | |
| borderRadius: 10, | |
| background: isCorrect ? "rgba(74, 222, 128, 0.15)" : "rgba(248, 113, 113, 0.15)", | |
| color: isCorrect ? "#4ade80" : "#f87171", | |
| fontWeight: 700, | |
| fontSize: 16 | |
| }}> | |
| {isCorrect ? "β Correct!" : `β It was ${currentLetter}`} | |
| </div> | |
| )} | |
| </div> | |
| ) : quizTotal === 0 ? ( | |
| <div style={{ textAlign: "center", color: "#666", padding: "60px 20px", fontSize: 15 }}> | |
| Click <strong>START NEW QUIZ</strong> to begin.<br /> | |
| Random letters will play at your chosen speed.<br /> | |
| Type the letter you hear and hit Submit (or Enter). | |
| </div> | |
| ) : null} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById("root")).render(<MorseTutor />); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment