Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions components/TypingGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const FALLBACK_CONTENT = 'fff jjj fff jjj';
const EN_DASH = '\u2013';

const TypingGame: React.FC<TypingGameProps> = ({ stage, subLevelId, content: contentProp, onFinish, onBack, onRetry, gameMode = 'STANDARD' }) => {
const { keyboardLayout } = useSettings();
const { keyboardLayout, zeroMistakesMode } = useSettings();
const { t, language } = useI18n();
const { playTyping, playError } = useSound();
const keyboardLayoutConfig = KEYBOARD_LAYOUTS[keyboardLayout];
Expand Down Expand Up @@ -143,7 +143,14 @@ const TypingGame: React.FC<TypingGameProps> = ({ stage, subLevelId, content: con
e.preventDefault();
if (startTime === null) setStartTime(Date.now());
if (key === targetKey) return; // Richtige Taste mit Modifier – nicht vorrücken, kein Fehler

playError();

if (zeroMistakesMode) {
onRetry();
return;
}

setMistakes(m => m + 1);
setErrorCountByChar(prev => {
const next = { ...prev };
Expand Down Expand Up @@ -198,6 +205,11 @@ const TypingGame: React.FC<TypingGameProps> = ({ stage, subLevelId, content: con

} else {
playError();
if (zeroMistakesMode) {
onRetry();
return;
}

setMistakes(m => m + 1);
setErrorCountByChar(prev => {
const next = { ...prev };
Expand All @@ -211,7 +223,7 @@ const TypingGame: React.FC<TypingGameProps> = ({ stage, subLevelId, content: con
setErrorKey(null);
}, 300);
}
}, [content, inputIndex, mistakes, finishGame, startTime, onBack, errorCountByChar, stage.id, playTyping, playError]);
}, [content, inputIndex, mistakes, finishGame, startTime, onBack, errorCountByChar, stage.id, playTyping, playError, zeroMistakesMode, onRetry]);

const handleKeyUp = useCallback((e: KeyboardEvent) => {
const normalized = (k: string) => (k === 'Minus' ? '-' : k === 'Comma' ? ',' : k === 'Period' ? '.' : k === 'Enter' ? '\n' : k);
Expand Down
29 changes: 28 additions & 1 deletion contexts/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { KeyboardLayout, Language } from '../types';
const LANGUAGE_STORAGE_KEY = 'tippsy_language';
const KEYBOARD_STORAGE_KEY = 'tippsy_keyboard_layout';
const SOUND_STORAGE_KEY = 'tippsy_sound_enabled';
const ZERO_MISTAKES_STORAGE_KEY = 'tippsy_zero_mistakes_mode';

const getStoredValue = (key: string) => {
try {
Expand Down Expand Up @@ -32,13 +33,21 @@ const getInitialSoundEnabled = (): boolean => {
return true;
};

const getInitialZeroMistakesMode = (): boolean => {
const stored = getStoredValue(ZERO_MISTAKES_STORAGE_KEY);
if (stored === '1' || stored === 'true') return true;
return false;
};

interface SettingsContextValue {
language: Language;
keyboardLayout: KeyboardLayout;
soundEnabled: boolean;
zeroMistakesMode: boolean;
setLanguage: (language: Language) => void;
setKeyboardLayout: (layout: KeyboardLayout) => void;
setSoundEnabled: (enabled: boolean) => void;
setZeroMistakesMode: (enabled: boolean) => void;
}

const SettingsContext = createContext<SettingsContextValue | undefined>(undefined);
Expand All @@ -47,6 +56,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const [language, setLanguage] = useState<Language>(() => getInitialLanguage());
const [keyboardLayout, setKeyboardLayout] = useState<KeyboardLayout>(() => getInitialKeyboardLayout(getInitialLanguage()));
const [soundEnabled, setSoundEnabled] = useState<boolean>(() => getInitialSoundEnabled());
const [zeroMistakesMode, setZeroMistakesMode] = useState<boolean>(() => getInitialZeroMistakesMode());

useEffect(() => {
try {
Expand Down Expand Up @@ -75,7 +85,24 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
}
}, [soundEnabled]);

const value = useMemo(() => ({ language, keyboardLayout, soundEnabled, setLanguage, setKeyboardLayout, setSoundEnabled }), [language, keyboardLayout, soundEnabled]);
useEffect(() => {
try {
localStorage.setItem(ZERO_MISTAKES_STORAGE_KEY, zeroMistakesMode ? '1' : '0');
} catch {
// ignore
}
}, [zeroMistakesMode]);

const value = useMemo(() => ({
language,
keyboardLayout,
soundEnabled,
zeroMistakesMode,
setLanguage,
setKeyboardLayout,
setSoundEnabled,
setZeroMistakesMode
}), [language, keyboardLayout, soundEnabled, zeroMistakesMode]);

return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
};
Expand Down
4 changes: 4 additions & 0 deletions i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ export const translations = {
languageHint: 'Choose the interface language and content.',
keyboardHint: 'Match your physical keyboard layout.',
soundHint: 'Play sounds when typing, on errors, and in the menu.',
zeroMistakes: 'Zero Mistake Mode',
zeroMistakesHint: 'Level restarts immediately upon any error. Only perfection counts!',
english: 'English (default)',
german: 'German',
qwerty: 'US QWERTY',
Expand Down Expand Up @@ -480,6 +482,8 @@ export const translations = {
languageHint: 'Wähle die Sprache für Oberfläche und Inhalte.',
keyboardHint: 'Passe das Layout an deine physische Tastatur an.',
soundHint: 'Sounds beim Tippen, bei Fehlern und im Menü abspielen.',
zeroMistakes: 'Zero Mistake Modus',
zeroMistakesHint: 'Level wird bei einem Fehler sofort neu gestartet. Nur Perfektion zählt!',
english: 'Englisch (Standard)',
german: 'Deutsch',
qwerty: 'US QWERTY',
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 22 additions & 1 deletion pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ interface SettingsProps {
}

const Settings: React.FC<SettingsProps> = ({ onBack }) => {
const { language, keyboardLayout, soundEnabled, setLanguage, setKeyboardLayout, setSoundEnabled } = useSettings();
const { language, keyboardLayout, soundEnabled, zeroMistakesMode, setLanguage, setKeyboardLayout, setSoundEnabled, setZeroMistakesMode } = useSettings();
const { t } = useI18n();
const { playMenuClick } = useSound();
const fileInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -194,6 +194,27 @@ const Settings: React.FC<SettingsProps> = ({ onBack }) => {
</div>
</div>

<div className="bg-slate-900/80 border border-slate-800 rounded-2xl p-6 mb-8">
<h2 className="text-lg font-bold text-white mb-2">{t('settings.zeroMistakes')}</h2>
<p className="text-slate-400 text-sm mb-6">{t('settings.zeroMistakesHint')}</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
onClick={() => setZeroMistakesMode(true)}
className={`px-4 py-3 rounded-xl border transition-all text-left ${zeroMistakesMode ? 'border-emerald-500 bg-emerald-500/10 text-white' : 'border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'}`}
>
<div className="font-bold">{t('settings.soundOn')}</div>
<div className="text-xs text-slate-400">{t('settings.zeroMistakes')}</div>
</button>
<button
onClick={() => setZeroMistakesMode(false)}
className={`px-4 py-3 rounded-xl border transition-all text-left ${!zeroMistakesMode ? 'border-emerald-500 bg-emerald-500/10 text-white' : 'border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'}`}
>
<div className="font-bold">{t('settings.soundOff')}</div>
<div className="text-xs text-slate-400">{t('settings.zeroMistakes')}</div>
</button>
</div>
</div>

<div className="bg-slate-900/80 border border-slate-800 rounded-2xl p-6">
<h2 className="text-lg font-bold text-white mb-2">{t('settings.data')}</h2>
<p className="text-slate-400 text-sm mb-6">{t('settings.exportHint')}</p>
Expand Down