Changes
This commit is contained in:
@@ -63,6 +63,25 @@ interface AchievementsContextType {
|
||||
unlockMaxScore: () => void;
|
||||
}
|
||||
|
||||
// Track time spent on each page
|
||||
const PAGE_TIME_KEY = 'page-time-tracking';
|
||||
|
||||
const getPageTimeTracking = (): Record<string, number> => {
|
||||
const saved = localStorage.getItem(PAGE_TIME_KEY);
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
};
|
||||
|
||||
const updatePageTime = (path: string, seconds: number): void => {
|
||||
const tracking = getPageTimeTracking();
|
||||
tracking[path] = (tracking[path] || 0) + seconds;
|
||||
localStorage.setItem(PAGE_TIME_KEY, JSON.stringify(tracking));
|
||||
};
|
||||
|
||||
export const getPageTime = (path: string): number => {
|
||||
const tracking = getPageTimeTracking();
|
||||
return tracking[path] || 0;
|
||||
};
|
||||
|
||||
const defaultAchievements: Achievement[] = [
|
||||
// Discovery achievements
|
||||
{ id: 'first_visit', name: 'Hello World', description: 'Visit the site for the first time', icon: '👋', unlocked: false },
|
||||
@@ -71,17 +90,17 @@ const defaultAchievements: Achievement[] = [
|
||||
{ id: 'terminal_user', name: 'Terminal Jockey', description: 'Use the terminal command interface', icon: '💻', unlocked: false },
|
||||
{ id: 'hint_seeker', name: 'Hint Seeker', description: 'Ask for a hint in the terminal', icon: '🔍', unlocked: false, secret: true },
|
||||
|
||||
// Navigation achievements
|
||||
{ id: 'home_visitor', name: 'Home Base', description: 'Visit the home page', icon: '🏠', unlocked: false },
|
||||
{ id: 'about_visitor', name: 'Getting Personal', description: 'Learn about the site owner', icon: '👤', unlocked: false },
|
||||
{ id: 'projects_visitor', name: 'Project Explorer', description: 'Check out the projects', icon: '📁', unlocked: false },
|
||||
{ id: 'resources_visitor', name: 'Resource Collector', description: 'Browse the resources page', icon: '📚', unlocked: false },
|
||||
{ id: 'links_visitor', name: 'Link Crawler', description: 'Visit the links page', icon: '🔗', unlocked: false },
|
||||
{ id: 'faq_visitor', name: 'Question Everything', description: 'Read the FAQ', icon: '❓', unlocked: false },
|
||||
{ id: 'music_visitor', name: 'DJ Mode', description: 'Open the music player', icon: '🎵', unlocked: false },
|
||||
{ id: 'ai_visitor', name: 'AI Whisperer', description: 'Chat with the AI', icon: '🤖', unlocked: false },
|
||||
{ id: 'arcade_visitor', name: 'Arcade Enthusiast', description: 'Visit the arcade', icon: '🕹️', unlocked: false },
|
||||
{ id: 'all_pages', name: 'Completionist', description: 'Visit every page on the site', icon: '🗺️', unlocked: false, secret: true },
|
||||
// Navigation achievements - now require 1 minute on page
|
||||
{ id: 'home_visitor', name: 'Home Base', description: 'Spend 1 minute on the home page', icon: '🏠', unlocked: false },
|
||||
{ id: 'about_visitor', name: 'Getting Personal', description: 'Spend 1 minute learning about the owner', icon: '👤', unlocked: false },
|
||||
{ id: 'projects_visitor', name: 'Project Explorer', description: 'Spend 1 minute exploring projects', icon: '📁', unlocked: false },
|
||||
{ id: 'resources_visitor', name: 'Resource Collector', description: 'Spend 1 minute browsing resources', icon: '📚', unlocked: false },
|
||||
{ id: 'links_visitor', name: 'Link Crawler', description: 'Spend 1 minute on the links page', icon: '🔗', unlocked: false },
|
||||
{ id: 'faq_visitor', name: 'Question Everything', description: 'Spend 1 minute reading the FAQ', icon: '❓', unlocked: false },
|
||||
{ id: 'music_visitor', name: 'DJ Mode', description: 'Spend 1 minute in the music player', icon: '🎵', unlocked: false },
|
||||
{ id: 'ai_visitor', name: 'AI Whisperer', description: 'Spend 1 minute chatting with AI', icon: '🤖', unlocked: false },
|
||||
{ id: 'arcade_visitor', name: 'Arcade Enthusiast', description: 'Spend 1 minute in the arcade', icon: '🕹️', unlocked: false },
|
||||
{ id: 'all_pages', name: 'Completionist', description: 'Spend 1 minute on every page', icon: '🗺️', unlocked: false, secret: true },
|
||||
|
||||
// Time achievements
|
||||
{ id: 'time_15min', name: 'Quick Visit', description: 'Spend 15 minutes on the site', icon: '⏱️', unlocked: false },
|
||||
@@ -183,9 +202,12 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
||||
if (hour >= 5 && hour < 7) unlockAchievement('early_bird');
|
||||
}, []);
|
||||
|
||||
// Track page visits
|
||||
// Track page visits and time spent
|
||||
const [currentPath, setCurrentPath] = useState(location.pathname);
|
||||
|
||||
useEffect(() => {
|
||||
const path = location.pathname;
|
||||
setCurrentPath(path);
|
||||
|
||||
setVisitedPages(prev => {
|
||||
const newSet = new Set(prev);
|
||||
@@ -194,30 +216,42 @@ export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
||||
return newSet;
|
||||
});
|
||||
|
||||
// Page-specific achievements
|
||||
if (path === '/') unlockAchievement('home_visitor');
|
||||
if (path === '/about') unlockAchievement('about_visitor');
|
||||
if (path === '/projects') unlockAchievement('projects_visitor');
|
||||
if (path === '/resources') unlockAchievement('resources_visitor');
|
||||
if (path === '/links') unlockAchievement('links_visitor');
|
||||
if (path === '/faq') unlockAchievement('faq_visitor');
|
||||
if (path === '/music') unlockAchievement('music_visitor');
|
||||
if (path === '/ai') unlockAchievement('ai_visitor');
|
||||
if (path === '/games') unlockAchievement('arcade_visitor');
|
||||
// Track time on page - increment every second
|
||||
const interval = setInterval(() => {
|
||||
updatePageTime(path, 1);
|
||||
const pageTime = getPageTime(path);
|
||||
|
||||
// Unlock achievements after 60 seconds (1 minute) on page
|
||||
if (pageTime >= 60) {
|
||||
if (path === '/') unlockAchievement('home_visitor');
|
||||
if (path === '/about') unlockAchievement('about_visitor');
|
||||
if (path === '/projects') unlockAchievement('projects_visitor');
|
||||
if (path === '/resources') unlockAchievement('resources_visitor');
|
||||
if (path === '/links') unlockAchievement('links_visitor');
|
||||
if (path === '/faq') unlockAchievement('faq_visitor');
|
||||
if (path === '/music') unlockAchievement('music_visitor');
|
||||
if (path === '/ai') unlockAchievement('ai_visitor');
|
||||
if (path === '/games') unlockAchievement('arcade_visitor');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Immediate unlocks for games and detail pages
|
||||
if (path.startsWith('/projects/')) unlockAchievement('project_detail');
|
||||
if (path === '/games/leaderboard') unlockAchievement('leaderboard_check');
|
||||
if (path === '/games/tetris') unlockAchievement('tetris_played');
|
||||
if (path === '/games/pacman') unlockAchievement('pacman_played');
|
||||
if (path === '/games/snake') unlockAchievement('snake_played');
|
||||
if (path === '/games/breakout') unlockAchievement('breakout_played');
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [location.pathname]);
|
||||
|
||||
// Check all pages visited
|
||||
// Check all pages visited (1 min on each)
|
||||
useEffect(() => {
|
||||
const requiredPages = ['/', '/about', '/projects', '/resources', '/links', '/faq', '/music', '/ai', '/games'];
|
||||
const allVisited = requiredPages.every(p => visitedPages.has(p));
|
||||
if (allVisited) unlockAchievement('all_pages');
|
||||
}, [visitedPages]);
|
||||
const allVisitedLongEnough = requiredPages.every(p => getPageTime(p) >= 60);
|
||||
if (allVisitedLongEnough) unlockAchievement('all_pages');
|
||||
}, [visitedPages, timeOnSite]); // Check periodically with timeOnSite
|
||||
|
||||
// Check all games played
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ interface SettingsContextType {
|
||||
setCrtEnabled: (enabled: boolean) => void;
|
||||
soundEnabled: boolean;
|
||||
setSoundEnabled: (enabled: boolean) => void;
|
||||
cryptoConsent: boolean | null; // null = never asked this session
|
||||
cryptoConsent: boolean | null;
|
||||
setCryptoConsent: (consent: boolean) => void;
|
||||
playSound: (type: SoundType) => void;
|
||||
hashrate: number;
|
||||
@@ -17,7 +17,12 @@ interface SettingsContextType {
|
||||
acceptedHashes: number;
|
||||
setAcceptedHashes: (hashes: number) => void;
|
||||
audioBlocked: boolean;
|
||||
resetAudioContext: () => void;
|
||||
showAudioOverlay: boolean;
|
||||
setShowAudioOverlay: (show: boolean) => void;
|
||||
enableAudio: () => void;
|
||||
disableAudio: () => void;
|
||||
userInteracted: boolean;
|
||||
setUserInteracted: (interacted: boolean) => void;
|
||||
}
|
||||
|
||||
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
||||
@@ -37,13 +42,12 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
// 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
|
||||
if (saved === null) return null;
|
||||
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
|
||||
// If declined (false), reset to null to re-prompt on new session
|
||||
const sessionPrompted = sessionStorage.getItem('cryptoConsentPrompted');
|
||||
if (parsed === false && !sessionPrompted) {
|
||||
return null; // Re-prompt
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
});
|
||||
@@ -55,33 +59,97 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
};
|
||||
|
||||
const [audioBlocked, setAudioBlocked] = useState(false);
|
||||
const [audioFailCount, setAudioFailCount] = useState(0);
|
||||
const [showAudioOverlay, setShowAudioOverlay] = useState(false);
|
||||
const [userInteracted, setUserInteracted] = useState(false);
|
||||
|
||||
const [hashrate, setHashrate] = useState(0);
|
||||
const [totalHashes, setTotalHashes] = useState(0);
|
||||
const [acceptedHashes, setAcceptedHashes] = useState(0);
|
||||
|
||||
// Single AudioContext instance, persisted across renders
|
||||
// Single AudioContext instance
|
||||
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]);
|
||||
|
||||
// Detect audio blocked and show overlay
|
||||
useEffect(() => {
|
||||
if (!soundEnabled) return;
|
||||
|
||||
// Check if we need to show the audio overlay
|
||||
const checkAudioState = () => {
|
||||
if (audioContextRef.current) {
|
||||
if (audioContextRef.current.state === 'suspended' && !userInteracted) {
|
||||
setAudioBlocked(true);
|
||||
setShowAudioOverlay(true);
|
||||
}
|
||||
} else {
|
||||
// Try to create AudioContext to check if it's blocked
|
||||
try {
|
||||
const testContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
if (testContext.state === 'suspended') {
|
||||
setAudioBlocked(true);
|
||||
setShowAudioOverlay(true);
|
||||
}
|
||||
audioContextRef.current = testContext;
|
||||
} catch (e) {
|
||||
console.warn('AudioContext creation failed:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Small delay to let page load
|
||||
const timeout = setTimeout(checkAudioState, 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [soundEnabled, userInteracted]);
|
||||
|
||||
// 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();
|
||||
audioContextRef.current.resume().catch(() => {
|
||||
setAudioBlocked(true);
|
||||
if (soundEnabledRef.current && !userInteracted) {
|
||||
setShowAudioOverlay(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return audioContextRef.current;
|
||||
}, [userInteracted]);
|
||||
|
||||
// Enable audio after user interaction
|
||||
const enableAudio = useCallback(() => {
|
||||
setUserInteracted(true);
|
||||
setShowAudioOverlay(false);
|
||||
|
||||
if (audioContextRef.current) {
|
||||
audioContextRef.current.resume().then(() => {
|
||||
setAudioBlocked(false);
|
||||
}).catch(console.warn);
|
||||
} else {
|
||||
try {
|
||||
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
audioContextRef.current.resume().then(() => {
|
||||
setAudioBlocked(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('AudioContext creation failed:', e);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Disable audio
|
||||
const disableAudio = useCallback(() => {
|
||||
setUserInteracted(true);
|
||||
setShowAudioOverlay(false);
|
||||
setSoundEnabled(false);
|
||||
setAudioBlocked(false);
|
||||
}, []);
|
||||
|
||||
// Cleanup on unmount
|
||||
@@ -102,33 +170,18 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
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;
|
||||
if (!soundEnabledRef.current || audioBlocked) 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;
|
||||
});
|
||||
setAudioBlocked(true);
|
||||
if (!userInteracted) {
|
||||
setShowAudioOverlay(true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -185,22 +238,15 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset fail count on success
|
||||
if (audioFailCount > 0) {
|
||||
setAudioFailCount(0);
|
||||
// Successfully played - audio is working
|
||||
if (audioBlocked) {
|
||||
setAudioBlocked(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Audio playback failed:', e);
|
||||
setAudioFailCount(prev => {
|
||||
const newCount = prev + 1;
|
||||
if (newCount >= 3) {
|
||||
setAudioBlocked(true);
|
||||
}
|
||||
return newCount;
|
||||
});
|
||||
setAudioBlocked(true);
|
||||
}
|
||||
}, [getAudioContext, audioFailCount]);
|
||||
}, [getAudioContext, audioBlocked, userInteracted]);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider
|
||||
@@ -219,7 +265,12 @@ export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
||||
acceptedHashes,
|
||||
setAcceptedHashes,
|
||||
audioBlocked,
|
||||
resetAudioContext,
|
||||
showAudioOverlay,
|
||||
setShowAudioOverlay,
|
||||
enableAudio,
|
||||
disableAudio,
|
||||
userInteracted,
|
||||
setUserInteracted,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user