aboutsummaryrefslogtreecommitdiff
path: root/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/components')
-rw-r--r--web/src/components/EndScreen.tsx43
-rw-r--r--web/src/components/MainMenu.tsx39
-rw-r--r--web/src/components/TypingGame.tsx29
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"/>