personal_website/src/contexts/SettingsContext.tsx
2025-12-08 12:19:53 +00:00

237 lines
7.3 KiB
TypeScript

import { createContext, useContext, useState, useEffect, useRef, useCallback, ReactNode } from 'react';
type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error';
interface SettingsContextType {
crtEnabled: boolean;
setCrtEnabled: (enabled: boolean) => void;
soundEnabled: boolean;
setSoundEnabled: (enabled: boolean) => void;
cryptoConsent: boolean | null; // null = never asked this session
setCryptoConsent: (consent: boolean) => void;
playSound: (type: SoundType) => void;
hashrate: number;
setHashrate: (rate: number) => void;
totalHashes: number;
setTotalHashes: (hashes: number) => void;
acceptedHashes: number;
setAcceptedHashes: (hashes: number) => void;
audioBlocked: boolean;
resetAudioContext: () => void;
}
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
export const SettingsProvider = ({ children }: { children: ReactNode }) => {
const [crtEnabled, setCrtEnabled] = useState(() => {
const saved = localStorage.getItem('crtEnabled');
return saved !== null ? JSON.parse(saved) : true;
});
const [soundEnabled, setSoundEnabled] = useState(() => {
const saved = localStorage.getItem('soundEnabled');
return saved !== null ? JSON.parse(saved) : true;
});
// Crypto consent: null = needs prompt, true = accepted, false = declined
// On session start, if user previously declined, we reset to null to re-prompt
const [cryptoConsent, setCryptoConsentState] = useState<boolean | null>(() => {
const saved = localStorage.getItem('cryptoConsent');
if (saved === null) return null; // Never set
const parsed = JSON.parse(saved);
// If declined (false), return null to re-prompt on new session
// We use sessionStorage to track if we've already shown the prompt this session
const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted');
if (parsed === false && !sessionPrompted) {
return null; // Re-prompt
}
return parsed;
});
const setCryptoConsent = (consent: boolean) => {
setCryptoConsentState(consent);
localStorage.setItem('cryptoConsent', JSON.stringify(consent));
sessionStorage.setItem('cryptoConsentPrompted', 'true');
};
const [audioBlocked, setAudioBlocked] = useState(false);
const [audioFailCount, setAudioFailCount] = useState(0);
const [hashrate, setHashrate] = useState(0);
const [totalHashes, setTotalHashes] = useState(0);
const [acceptedHashes, setAcceptedHashes] = useState(0);
// Single AudioContext instance, persisted across renders
const audioContextRef = useRef<AudioContext | null>(null);
// Use ref to always have current soundEnabled value in callbacks
const soundEnabledRef = useRef(soundEnabled);
useEffect(() => {
soundEnabledRef.current = soundEnabled;
}, [soundEnabled]);
// Get or create AudioContext
const getAudioContext = useCallback(() => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
// Resume if suspended (browser autoplay policy)
if (audioContextRef.current.state === 'suspended') {
audioContextRef.current.resume();
}
return audioContextRef.current;
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
};
}, []);
useEffect(() => {
localStorage.setItem('crtEnabled', JSON.stringify(crtEnabled));
}, [crtEnabled]);
useEffect(() => {
localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled));
}, [soundEnabled]);
// Reset audio context to try again after user interaction
const resetAudioContext = useCallback(() => {
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
setAudioBlocked(false);
setAudioFailCount(0);
}, []);
const playSound = useCallback((type: SoundType) => {
// Use ref to ensure we always have the latest value
if (!soundEnabledRef.current) return;
try {
const audioContext = getAudioContext();
// Check if context is blocked
if (audioContext.state === 'suspended') {
audioContext.resume().catch(() => {
setAudioFailCount(prev => {
const newCount = prev + 1;
if (newCount >= 3) {
setAudioBlocked(true);
}
return newCount;
});
});
return;
}
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
const now = audioContext.currentTime;
switch (type) {
case 'click':
oscillator.frequency.value = 800;
gainNode.gain.value = 0.1;
oscillator.start(now);
oscillator.stop(now + 0.05);
break;
case 'beep':
oscillator.frequency.value = 1200;
gainNode.gain.value = 0.08;
oscillator.start(now);
oscillator.stop(now + 0.1);
break;
case 'hover':
oscillator.frequency.value = 600;
gainNode.gain.value = 0.05;
oscillator.start(now);
oscillator.stop(now + 0.03);
break;
case 'boot':
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(200, now);
oscillator.frequency.exponentialRampToValueAtTime(800, now + 0.2);
gainNode.gain.setValueAtTime(0.1, now);
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
oscillator.start(now);
oscillator.stop(now + 0.3);
break;
case 'success':
oscillator.frequency.setValueAtTime(800, now);
oscillator.frequency.setValueAtTime(1200, now + 0.1);
gainNode.gain.value = 0.1;
oscillator.start(now);
oscillator.stop(now + 0.2);
break;
case 'error':
oscillator.type = 'square';
oscillator.frequency.value = 150;
gainNode.gain.value = 0.08;
oscillator.start(now);
oscillator.stop(now + 0.15);
break;
}
// Reset fail count on success
if (audioFailCount > 0) {
setAudioFailCount(0);
setAudioBlocked(false);
}
} catch (e) {
console.warn('Audio playback failed:', e);
setAudioFailCount(prev => {
const newCount = prev + 1;
if (newCount >= 3) {
setAudioBlocked(true);
}
return newCount;
});
}
}, [getAudioContext, audioFailCount]);
return (
<SettingsContext.Provider
value={{
crtEnabled,
setCrtEnabled,
soundEnabled,
setSoundEnabled,
cryptoConsent,
setCryptoConsent,
playSound,
hashrate,
setHashrate,
totalHashes,
setTotalHashes,
acceptedHashes,
setAcceptedHashes,
audioBlocked,
resetAudioContext,
}}
>
{children}
</SettingsContext.Provider>
);
};
export const useSettings = () => {
const context = useContext(SettingsContext);
if (context === undefined) {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
};