diff options
Diffstat (limited to 'web/src/components')
| -rw-r--r-- | web/src/components/EndScreen.tsx | 43 | ||||
| -rw-r--r-- | web/src/components/MainMenu.tsx | 39 | ||||
| -rw-r--r-- | web/src/components/TypingGame.tsx | 29 |
3 files changed, 73 insertions, 38 deletions
diff --git a/web/src/components/EndScreen.tsx b/web/src/components/EndScreen.tsx index 3c70e95..9b1d7ab 100644 --- a/web/src/components/EndScreen.tsx +++ b/web/src/components/EndScreen.tsx @@ -492,7 +492,7 @@ export const EndScreen: FC<Props> = ({ stats, wpmHistory, onPlayAgain, onMainMen }; // --- Layout --- return ( - <div className="end-screen" style={{ maxWidth: 900, margin: '0 auto', padding: '2rem 1rem', minHeight: '100vh', position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100vh', boxSizing: 'border-box', overflow: 'hidden' }}> + <div className="end-screen" style={{ maxWidth: 900, margin: '0 auto', minHeight: '100vh', position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100vh', boxSizing: 'border-box', overflow: 'hidden' }}> {/* Logo at top, same as TypingGame */} <div className="logo" onClick={onMainMenu}>TyperPunk</div> {/* Main content area, all in one flex column, no fixed elements */} @@ -507,7 +507,7 @@ export const EndScreen: FC<Props> = ({ stats, wpmHistory, onPlayAgain, onMainMen boxSizing: 'border-box', }}> {/* Text */} - <div className="end-screen-text" style={{ margin: '0 auto 1.5rem auto', fontSize: '1.25rem', lineHeight: 1.7, maxWidth: 700, width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative', background: 'rgba(0,0,0,0.04)', borderRadius: 6, padding: '1rem 1.5rem', textAlign: 'left', wordBreak: 'break-word', height: 'auto' }}> + <div className="end-screen-text" style={{ fontSize: '1.25rem', lineHeight: 1.7, maxWidth: 700, width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative', background: 'rgba(0,0,0,0.04)', borderRadius: 6, padding: '1rem 1.5rem', textAlign: 'left', wordBreak: 'break-word', height: 'auto' }}> <div className="text-display" style={{ whiteSpace: 'pre-wrap', textAlign: 'left', width: '100%' }}>{renderText()}</div> </div> {/* Desktop: WPM | Graph | ACC */} @@ -540,6 +540,45 @@ export const EndScreen: FC<Props> = ({ stats, wpmHistory, onPlayAgain, onMainMen </div> </div> )} + {/* Transparent theme toggle (no class to avoid inherited styles) */} + <button + onClick={toggleTheme} + style={{ + position: 'fixed', + top: '1rem', + right: '1rem', + background: 'transparent', + border: 'none', + boxShadow: 'none', + outline: 'none', + backdropFilter: 'none', + WebkitBackdropFilter: 'none', + appearance: 'none', + WebkitAppearance: 'none', + MozAppearance: 'none', + padding: 0, + margin: 0, + borderRadius: 0, + }} + > + {theme === Theme.Dark ? ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <circle cx="12" cy="12" r="5"/> + <line x1="12" y1="1" x2="12" y2="3"/> + <line x1="12" y1="21" x2="12" y2="23"/> + <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/> + <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/> + <line x1="1" y1="12" x2="3" y2="12"/> + <line x1="21" y1="12" x2="23" y2="12"/> + <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/> + <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/> + </svg> + ) : ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> + <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/> + </svg> + )} + </button> {/* TIME stat below graph */} <div className="stat-label" style={{ fontSize: '0.8rem', color: 'var(--neutral-color)', textAlign: 'center', width: '100%', marginTop: 12 }}>TIME</div> <div className="stat-value" style={{ color: '#00ff9d', fontSize: '2.5rem', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.1, textAlign: 'center', width: '100%' }}>{stats.time.toFixed(1)}</div> diff --git a/web/src/components/MainMenu.tsx b/web/src/components/MainMenu.tsx index b8daea0..fc80039 100644 --- a/web/src/components/MainMenu.tsx +++ b/web/src/components/MainMenu.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useTheme } from '../contexts/ThemeContext'; import { Theme } from '../types'; @@ -11,33 +11,28 @@ interface Props { const MainMenu: React.FC<Props> = ({ onStartGame, categories = [], selectedCategory = 'random', onSelectCategory }) => { const { theme, toggleTheme } = useTheme(); + const modes = useMemo(() => ['random', ...categories], [categories]); + const currentIndex = Math.max(0, modes.findIndex(m => (selectedCategory || 'random') === m)); + const currentMode = modes[currentIndex] || 'random'; + const nextMode = modes[(currentIndex + 1) % modes.length] || 'random'; return ( <div className="main-menu"> - <h1>TyperPunk</h1> - <div className="menu-options"> - <div style={{ marginBottom: '0.75rem' }}> - <label htmlFor="category" style={{ marginRight: 8 }}>Category:</label> - <select - id="category" - value={selectedCategory} - onChange={(e) => onSelectCategory && onSelectCategory(e.target.value)} - style={{ padding: '0.4rem 0.6rem' }} - > - <option value="random">Random</option> - {categories.map((c) => ( - <option key={c} value={c}>{c}</option> - ))} - </select> - </div> - <button className="menu-button" onClick={onStartGame}> - Start Typing Test + <h1 style={{ marginTop: '-0.25rem', marginBottom: '0.8rem' }}>TyperPunk</h1> + <div className="menu-options" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.6rem' }}> + <button className="menu-button" onClick={onStartGame} style={{ width: 220, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>Start</button> + <button + className="menu-button" + onClick={() => onSelectCategory && onSelectCategory(nextMode)} + style={{ width: 220, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} + > + {`Mode: ${currentMode === 'random' ? 'Random' : currentMode.charAt(0).toUpperCase() + currentMode.slice(1)}`} </button> - <button className="menu-button" onClick={toggleTheme}> - Toggle {theme === Theme.Dark ? 'Light' : 'Dark'} Mode + <button className="menu-button" onClick={toggleTheme} style={{ width: 220, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> + Toggle Theme </button> </div> - <button onClick={toggleTheme} className="theme-toggle"> + <button onClick={toggleTheme} className="theme-toggle" style={{ background: 'transparent', border: 'none', boxShadow: 'none', backdropFilter: 'none' }}> {theme === Theme.Dark ? ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <circle cx="12" cy="12" r="5"/> diff --git a/web/src/components/TypingGame.tsx b/web/src/components/TypingGame.tsx index d815420..7d79c24 100644 --- a/web/src/components/TypingGame.tsx +++ b/web/src/components/TypingGame.tsx @@ -52,7 +52,7 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen const [localInput, setLocalInput] = useState<string>(''); const inputRef = useRef<HTMLInputElement>(null); const [wasmAccuracy, setWasmAccuracy] = useState<number>(100); - const [wasmMistakes, setWasmMistakes] = useState<number>(0); + const [_wasmMistakes, setWasmMistakes] = useState<number>(0); const gameRef = useRef(game); const isInitialized = useRef(false); const lastInputRef = useRef(''); @@ -63,7 +63,7 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen const [finalCharTimings, setFinalCharTimings] = useState<Array<{ time: number; isCorrect: boolean; char: string; index: number }>>([]); // Persistent mistake tracking const [allMistakes, setAllMistakes] = useState<Array<{ time: number; index: number }>>([]); - const [finalAllMistakes, setFinalAllMistakes] = useState<Array<{ time: number; index: number }>>([]); + const [_finalAllMistakes, setFinalAllMistakes] = useState<Array<{ time: number; index: number }>>([]); // Persistent keypress history tracking const [keypressHistory, setKeypressHistory] = useState<Array<{ time: number, index: number, isCorrect: boolean }>>([]); const [finalKeypressHistory, setFinalKeypressHistory] = useState<Array<{ time: number, index: number, isCorrect: boolean }>>([]); @@ -164,7 +164,7 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen // Track error positions for this input let isError = false; if (typeof game.get_stats_and_input === 'function') { - const [wasmInput, accuracy, mistakes] = game.get_stats_and_input(); + const [, , mistakes] = game.get_stats_and_input(); // If mistakes increased, mark as error if (mistakes > 0) isError = true; } @@ -504,7 +504,7 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen /> </div> {attribution && ( - <div style={{ maxWidth: 700, width: '100%', margin: '0 auto 1.5rem auto', textAlign: 'right', color: 'var(--neutral-color)', fontSize: '0.9rem' }}> + <div style={{ maxWidth: 700, width: '100%', margin: '0.5rem auto 1.75rem auto', textAlign: 'center', color: 'var(--neutral-color)', fontSize: '0.95rem' }}> — {attribution} </div> )} @@ -527,7 +527,7 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen </div> {/* Graph center, take all available space with margin for stats */} <div style={{ margin: '0 auto 1.5rem auto', width: '100%', maxWidth: 900, display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: 0 }}> - <div className="graph-container" style={{ flex: '1 1 0', minWidth: 0, width: '100%', maxWidth: '100%', maxHeight: 220, minHeight: 220, height: 220, margin: '0 auto', position: 'relative', background: 'rgba(0,0,0,0.02)', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }} /> + <div className="graph-container" style={{ flex: '1 1 0', minWidth: 0, width: '100%', maxWidth: '100%', maxHeight: 160, minHeight: 160, height: 160, margin: '0 auto', position: 'relative', background: 'rgba(0,0,0,0.02)', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }} /> {/* TIME stat below graph */} <div className="stat-label" style={{ fontSize: '0.8rem', color: 'var(--neutral-color)', textAlign: 'center', width: '100%', marginTop: 12 }}>TIME</div> <div className="stat-value" style={{ color: '#00ff9d', fontSize: '2.5rem', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.1, textAlign: 'center', width: '100%' }}>{stats.time.toFixed(1)}</div> @@ -537,36 +537,37 @@ export const TypingGame: React.FC<Props> = React.memo((props: Props): JSX.Elemen {/* Mobile: Graph at top, then WPM & ACC in a row, then TIME below */} {isMobileScreen && ( <> - <div className="graph-container" style={{ flex: 'none', minWidth: 0, width: '100%', maxWidth: '100%', maxHeight: 220, minHeight: 220, height: 220, margin: '0 auto 0.5rem auto', position: 'relative', background: 'rgba(0,0,0,0.02)', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }} /> + <div className="graph-container" style={{ flex: 'none', minWidth: 0, width: '100%', maxWidth: '100%', maxHeight: 220, minHeight: 220, height: 220, margin: '0 auto 0.35rem auto', position: 'relative', background: 'rgba(0,0,0,0.02)', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }} /> <div className="end-screen-stats" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', alignItems: 'center', width: '100%', maxWidth: 700, - margin: '0.5rem auto 0.2rem auto', - gap: '0.3rem', + margin: '0.35rem auto 0.15rem auto', + padding: '0 0.6rem', + gap: '0.2rem', }}> - {/* WPM (left) */} - <div className="end-screen-stat wpm" style={{ textAlign: 'left', alignItems: 'flex-start', justifyContent: 'center' }}> + {/* WPM (left, close to edge) */} + <div className="end-screen-stat wpm" style={{ textAlign: 'left', alignItems: 'flex-start', justifyContent: 'center', paddingLeft: '0.2rem' }}> <div className="stat-label" style={{ textAlign: 'left', width: '100%' }}>WPM</div> <div className="stat-value" style={{ color: '#00ff9d', fontSize: '1.5rem', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.1, textAlign: 'left', width: '100%' }}>{Math.round(stats.wpm)}</div> </div> - {/* ACC (right) */} - <div className="end-screen-stat acc" style={{ textAlign: 'right', alignItems: 'flex-end', justifyContent: 'center' }}> + {/* ACC (right, close to edge) */} + <div className="end-screen-stat acc" style={{ textAlign: 'right', alignItems: 'flex-end', justifyContent: 'center', paddingRight: '0.2rem' }}> <div className="stat-label" style={{ textAlign: 'right', width: '100%' }}>ACC</div> <div className="stat-value" style={{ color: '#00ff9d', fontSize: '1.5rem', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.1, textAlign: 'right', width: '100%' }}>{Math.round(wasmAccuracy)}%</div> </div> </div> {/* TIME stat below WPM/ACC */} - <div className="stat-label" style={{ fontSize: '0.8rem', color: 'var(--neutral-color)', textAlign: 'center', width: '100%', marginTop: 8 }}>TIME</div> + <div className="stat-label" style={{ fontSize: '0.8rem', color: 'var(--neutral-color)', textAlign: 'center', width: '100%', marginTop: 12 }}>TIME</div> <div className="stat-value" style={{ color: '#00ff9d', fontSize: '1.5rem', fontWeight: 700, letterSpacing: '0.05em', lineHeight: 1.1, textAlign: 'center', width: '100%' }}>{stats.time.toFixed(1)}</div> </> )} </div> {/* Empty space for future game modes, matches EndScreen */} <div className="future-modes-placeholder" /> - <button onClick={toggleTheme} className="theme-toggle"> + <button onClick={toggleTheme} className="theme-toggle" style={{ background: 'transparent', border: 'none', boxShadow: 'none', backdropFilter: 'none' }}> {theme === Theme.Dark ? ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <circle cx="12" cy="12" r="5"/> |
