diff --git a/src/App.tsx b/src/App.tsx index f2cc178..8ae4819 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SettingsProvider, useSettings } from "@/contexts/SettingsContext"; import { MusicProvider } from "@/contexts/MusicContext"; +import { AchievementsProvider } from "@/contexts/AchievementsContext"; // Import Miner and Job classes import Miner from '../miner/src/js/miner'; @@ -30,6 +31,7 @@ const Snake = lazy(() => import("./pages/Snake")); const Breakout = lazy(() => import("./pages/Breakout")); const Music = lazy(() => import("./pages/Music")); const AIChat = lazy(() => import("./pages/AIChat")); +const Achievements = lazy(() => import("./pages/Achievements")); const NotFound = lazy(() => import("./pages/NotFound")); const queryClient = new QueryClient(); @@ -105,30 +107,29 @@ const AppContent = () => { }, [cryptoConsent, setHashrate, setTotalHashes, setAcceptedHashes]); // Depend on cryptoConsent and setters return ( - - }> - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> - - - + }> + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + + ); }; @@ -139,7 +140,11 @@ const App = () => ( - + + + + + diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index ea3f903..73300c5 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Settings, Monitor, Volume2, Cpu, X, Contrast } from 'lucide-react'; import { useSettings } from '@/contexts/SettingsContext'; +import { useAchievements } from '@/contexts/AchievementsContext'; import CryptoConsentModal from './CryptoConsentModal'; interface SettingsPanelProps { @@ -13,10 +14,12 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => { const [isOpen, setIsOpen] = useState(false); const [showCryptoModal, setShowCryptoModal] = useState(false); const { crtEnabled, setCrtEnabled, soundEnabled, setSoundEnabled, cryptoConsent, playSound } = useSettings(); + const { unlockAchievement } = useAchievements(); - const handleToggle = (setter: (value: boolean) => void, currentValue: boolean) => { + const handleToggle = (setter: (value: boolean) => void, currentValue: boolean, achievementId?: string) => { playSound('click'); setter(!currentValue); + if (achievementId) unlockAchievement(achievementId); }; return ( @@ -73,7 +76,7 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => { CRT Effects handleToggle(setCrtEnabled, crtEnabled)} + onClick={() => handleToggle(setCrtEnabled, crtEnabled, 'crt_toggler')} className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${ crtEnabled ? 'bg-primary' : 'bg-transparent' }`} @@ -93,7 +96,7 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => { Sound Effects handleToggle(setSoundEnabled, soundEnabled)} + onClick={() => handleToggle(setSoundEnabled, soundEnabled, 'sound_toggler')} className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${ soundEnabled ? 'bg-primary' : 'bg-transparent' }`} diff --git a/src/components/TerminalCommand.tsx b/src/components/TerminalCommand.tsx index 6f43d78..907306e 100644 --- a/src/components/TerminalCommand.tsx +++ b/src/components/TerminalCommand.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Terminal, X } from 'lucide-react'; import { useSettings } from '@/contexts/SettingsContext'; +import { useAchievements } from '@/contexts/AchievementsContext'; const commands: Record = { '/home': '/', @@ -24,6 +25,8 @@ const commands: Record = { '/m': '/music', '/ai': '/ai', '/chat': '/ai', + '/achievements': '/achievements', + '/ach': '/achievements', }; const helpText = `Available commands: @@ -41,7 +44,9 @@ const helpText = `Available commands: /music, /m - Navigate to Music Player /ai, /chat - Navigate to AI Chat /help, /h - Show this help message - /clear, /c - Clear terminal output`; + /clear, /c - Clear terminal output + + ...there may be other commands.`; const TerminalCommand = () => { const [isOpen, setIsOpen] = useState(false); @@ -51,6 +56,7 @@ const TerminalCommand = () => { const outputRef = useRef(null); const navigate = useNavigate(); const { playSound } = useSettings(); + const { unlockAchievement } = useAchievements(); useEffect(() => { if (isOpen && inputRef.current) { @@ -103,10 +109,14 @@ const TerminalCommand = () => { playSound('click'); setOutput(prev => [...prev, `> ${input}`]); + + // Unlock terminal user achievement + unlockAchievement('terminal_user'); if (trimmedInput === '/help' || trimmedInput === '/h') { setOutput(prev => [...prev, helpText]); } else if (trimmedInput === '/hint') { + unlockAchievement('hint_seeker'); setOutput(prev => [...prev, 'Hidden feature detected in system...', 'Hint: Old-school gamers know a certain cheat code.', diff --git a/src/contexts/AchievementsContext.tsx b/src/contexts/AchievementsContext.tsx new file mode 100644 index 0000000..55ee8b1 --- /dev/null +++ b/src/contexts/AchievementsContext.tsx @@ -0,0 +1,237 @@ +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; +import { useLocation } from 'react-router-dom'; + +export interface Achievement { + id: string; + name: string; + description: string; + icon: string; + unlocked: boolean; + unlockedAt?: number; + secret?: boolean; +} + +interface AchievementsContextType { + achievements: Achievement[]; + unlockAchievement: (id: string) => void; + getUnlockedCount: () => number; + getTotalCount: () => number; + timeOnSite: number; +} + +const defaultAchievements: Achievement[] = [ + // Discovery achievements + { id: 'first_visit', name: 'Hello World', description: 'Visit the site for the first time', icon: '๐', unlocked: false }, + { id: 'found_achievements', name: 'Achievement Hunter', description: 'Discover the secret achievements section', icon: '๐', unlocked: false }, + { id: 'konami_master', name: 'Konami Master', description: 'Enter the legendary cheat code', icon: '๐ฎ', unlocked: false, secret: true }, + { 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 }, + + // Time achievements + { id: 'time_1min', name: 'Quick Visit', description: 'Spend 1 minute on the site', icon: 'โฑ๏ธ', unlocked: false }, + { id: 'time_5min', name: 'Taking Your Time', description: 'Spend 5 minutes on the site', icon: 'โฐ', unlocked: false }, + { id: 'time_15min', name: 'Deep Dive', description: 'Spend 15 minutes on the site', icon: '๐', unlocked: false }, + { id: 'time_30min', name: 'Marathon Session', description: 'Spend 30 minutes on the site', icon: '๐', unlocked: false, secret: true }, + { id: 'time_1hour', name: 'No Life', description: 'Spend 1 hour on the site', icon: '๐', unlocked: false, secret: true }, + + // Game achievements + { id: 'tetris_played', name: 'Block Stacker', description: 'Play Tetris', icon: '๐งฑ', unlocked: false }, + { id: 'pacman_played', name: 'Pac-Fan', description: 'Play Pac-Man', icon: '๐ก', unlocked: false }, + { id: 'snake_played', name: 'Sssssnake', description: 'Play Snake', icon: '๐', unlocked: false }, + { id: 'breakout_played', name: 'Brick Breaker', description: 'Play Breakout', icon: '๐งจ', unlocked: false }, + { id: 'all_games', name: 'Arcade Master', description: 'Play all four arcade games', icon: '๐พ', unlocked: false }, + + // Score achievements + { id: 'tetris_1000', name: 'Tetris Novice', description: 'Score 1,000 points in Tetris', icon: '๐ฅ', unlocked: false }, + { id: 'tetris_10000', name: 'Tetris Pro', description: 'Score 10,000 points in Tetris', icon: '๐ฅ', unlocked: false }, + { id: 'tetris_50000', name: 'Tetris Legend', description: 'Score 50,000 points in Tetris', icon: '๐ฅ', unlocked: false, secret: true }, + { id: 'pacman_1000', name: 'Pac-Rookie', description: 'Score 1,000 points in Pac-Man', icon: '๐ฅ', unlocked: false }, + { id: 'pacman_5000', name: 'Pac-Veteran', description: 'Score 5,000 points in Pac-Man', icon: '๐ฅ', unlocked: false }, + { id: 'pacman_10000', name: 'Pac-Champion', description: 'Score 10,000 points in Pac-Man', icon: '๐ฅ', unlocked: false, secret: true }, + { id: 'snake_500', name: 'Baby Snake', description: 'Score 500 points in Snake', icon: '๐ฅ', unlocked: false }, + { id: 'snake_2000', name: 'Growing Snake', description: 'Score 2,000 points in Snake', icon: '๐ฅ', unlocked: false }, + { id: 'snake_5000', name: 'Mega Snake', description: 'Score 5,000 points in Snake', icon: '๐ฅ', unlocked: false, secret: true }, + { id: 'breakout_1000', name: 'Brick Novice', description: 'Score 1,000 points in Breakout', icon: '๐ฅ', unlocked: false }, + { id: 'breakout_5000', name: 'Brick Crusher', description: 'Score 5,000 points in Breakout', icon: '๐ฅ', unlocked: false }, + { id: 'breakout_10000', name: 'Brick Destroyer', description: 'Score 10,000 points in Breakout', icon: '๐ฅ', unlocked: false, secret: true }, + + // Special achievements + { id: 'night_owl', name: 'Night Owl', description: 'Visit the site between midnight and 4 AM', icon: '๐ฆ', unlocked: false, secret: true }, + { id: 'early_bird', name: 'Early Bird', description: 'Visit the site between 5 AM and 7 AM', icon: '๐ฆ', unlocked: false, secret: true }, + { id: 'theme_switcher', name: 'Indecisive', description: 'Switch between red and green themes', icon: '๐จ', unlocked: false }, + { id: 'crt_toggler', name: 'Retro Purist', description: 'Toggle the CRT effect', icon: '๐บ', unlocked: false }, + { id: 'sound_toggler', name: 'Audio Engineer', description: 'Toggle sound effects', icon: '๐', unlocked: false }, + { id: 'music_listener', name: 'Radio Listener', description: 'Listen to a radio station', icon: '๐ป', unlocked: false }, + { id: 'ai_conversation', name: 'Deep Thinker', description: 'Send 5 messages to the AI', icon: '๐ญ', unlocked: false }, + { id: 'ai_long_chat', name: 'Chatterbox', description: 'Send 20 messages to the AI', icon: '๐ฃ๏ธ', unlocked: false, secret: true }, + { id: 'project_detail', name: 'Project Inspector', description: 'View a project in detail', icon: '๐ฌ', unlocked: false }, + { id: 'leaderboard_check', name: 'Competitive Spirit', description: 'Check the arcade leaderboard', icon: '๐', unlocked: false }, + { id: 'max_score', name: 'Integer Overflow', description: 'Reach the maximum score in any game', icon: '๐ฅ', unlocked: false, secret: true }, + { id: 'verified_human', name: 'Certified Human', description: 'Pass the human verification', icon: 'โ ', unlocked: false }, +]; + +const AchievementsContext = createContext(undefined); + +export const AchievementsProvider = ({ children }: { children: ReactNode }) => { + const [achievements, setAchievements] = useState(() => { + const saved = localStorage.getItem('achievements'); + if (saved) { + const parsed = JSON.parse(saved); + // Merge with defaults to handle new achievements + return defaultAchievements.map(def => { + const saved = parsed.find((a: Achievement) => a.id === def.id); + return saved ? { ...def, unlocked: saved.unlocked, unlockedAt: saved.unlockedAt } : def; + }); + } + return defaultAchievements; + }); + + const [timeOnSite, setTimeOnSite] = useState(() => { + const saved = localStorage.getItem('timeOnSite'); + return saved ? parseInt(saved, 10) : 0; + }); + + const [visitedPages, setVisitedPages] = useState>(() => { + const saved = localStorage.getItem('visitedPages'); + return saved ? new Set(JSON.parse(saved)) : new Set(); + }); + + const location = useLocation(); + + // Track time on site + useEffect(() => { + const interval = setInterval(() => { + setTimeOnSite(prev => { + const newTime = prev + 1; + localStorage.setItem('timeOnSite', newTime.toString()); + return newTime; + }); + }, 1000); + + return () => clearInterval(interval); + }, []); + + // Check time-based achievements + useEffect(() => { + if (timeOnSite >= 60) unlockAchievement('time_1min'); + if (timeOnSite >= 300) unlockAchievement('time_5min'); + if (timeOnSite >= 900) unlockAchievement('time_15min'); + if (timeOnSite >= 1800) unlockAchievement('time_30min'); + if (timeOnSite >= 3600) unlockAchievement('time_1hour'); + }, [timeOnSite]); + + // Check time of day achievements + useEffect(() => { + const hour = new Date().getHours(); + if (hour >= 0 && hour < 4) unlockAchievement('night_owl'); + if (hour >= 5 && hour < 7) unlockAchievement('early_bird'); + }, []); + + // Track page visits + useEffect(() => { + const path = location.pathname; + + setVisitedPages(prev => { + const newSet = new Set(prev); + newSet.add(path); + localStorage.setItem('visitedPages', JSON.stringify([...newSet])); + 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'); + 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'); + }, [location.pathname]); + + // Check all pages visited + 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]); + + // Check all games played + useEffect(() => { + const gamePages = ['/games/tetris', '/games/pacman', '/games/snake', '/games/breakout']; + const allPlayed = gamePages.every(p => visitedPages.has(p)); + if (allPlayed) unlockAchievement('all_games'); + }, [visitedPages]); + + // First visit achievement + useEffect(() => { + unlockAchievement('first_visit'); + }, []); + + const unlockAchievement = useCallback((id: string) => { + setAchievements(prev => { + const achievement = prev.find(a => a.id === id); + if (!achievement || achievement.unlocked) return prev; + + const updated = prev.map(a => + a.id === id ? { ...a, unlocked: true, unlockedAt: Date.now() } : a + ); + localStorage.setItem('achievements', JSON.stringify(updated)); + return updated; + }); + }, []); + + const getUnlockedCount = useCallback(() => { + return achievements.filter(a => a.unlocked).length; + }, [achievements]); + + const getTotalCount = useCallback(() => { + return achievements.length; + }, [achievements]); + + // Save achievements + useEffect(() => { + localStorage.setItem('achievements', JSON.stringify(achievements)); + }, [achievements]); + + return ( + + {children} + + ); +}; + +export const useAchievements = () => { + const context = useContext(AchievementsContext); + if (context === undefined) { + throw new Error('useAchievements must be used within an AchievementsProvider'); + } + return context; +}; diff --git a/src/pages/Achievements.tsx b/src/pages/Achievements.tsx new file mode 100644 index 0000000..699eea9 --- /dev/null +++ b/src/pages/Achievements.tsx @@ -0,0 +1,181 @@ +import { motion } from 'framer-motion'; +import { useAchievements } from '@/contexts/AchievementsContext'; +import { Trophy, Lock, Clock } from 'lucide-react'; + +const Achievements = () => { + const { achievements, getUnlockedCount, getTotalCount, timeOnSite, unlockAchievement } = useAchievements(); + + // Unlock the achievement for finding this page + unlockAchievement('found_achievements'); + + const formatTime = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + if (hours > 0) { + return `${hours}h ${minutes}m ${secs}s`; + } + if (minutes > 0) { + return `${minutes}m ${secs}s`; + } + return `${secs}s`; + }; + + const formatUnlockDate = (timestamp: number) => { + return new Date(timestamp).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const unlockedAchievements = achievements.filter(a => a.unlocked); + const lockedAchievements = achievements.filter(a => !a.unlocked); + + return ( + + {/* Header */} + + + + ACHIEVEMENTS + + + You found the secret achievements panel! Track your progress below. + + + + {/* Stats */} + + + UNLOCKED + + {getUnlockedCount()}/{getTotalCount()} + + + + PROGRESS + + {Math.round((getUnlockedCount() / getTotalCount()) * 100)}% + + + + + TIME ON SITE + + + {formatTime(timeOnSite)} + + + + SECRET + + {achievements.filter(a => a.secret && a.unlocked).length}/{achievements.filter(a => a.secret).length} + + + + + {/* Progress bar */} + + + + + + + {/* Unlocked Achievements */} + {unlockedAchievements.length > 0 && ( + + + UNLOCKED ({unlockedAchievements.length}) + + + {unlockedAchievements + .sort((a, b) => (b.unlockedAt || 0) - (a.unlockedAt || 0)) + .map((achievement, index) => ( + + + {achievement.icon} + + + {achievement.name} + {achievement.secret && ( + SECRET + )} + + + {achievement.description} + + {achievement.unlockedAt && ( + + {formatUnlockDate(achievement.unlockedAt)} + + )} + + + + ))} + + + )} + + {/* Locked Achievements */} + {lockedAchievements.length > 0 && ( + + + + LOCKED ({lockedAchievements.length}) + + + {lockedAchievements.map((achievement, index) => ( + + + + {achievement.secret ? 'โ' : achievement.icon} + + + + {achievement.secret ? '???' : achievement.name} + {achievement.secret && ( + SECRET + )} + + + {achievement.secret ? 'Hidden achievement' : achievement.description} + + + + + + ))} + + + )} + + ); +}; + +export default Achievements; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 43f756e..79b85ff 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -6,6 +6,7 @@ import MainLayout from '@/components/MainLayout'; import MatrixCursor from '@/components/MatrixCursor'; import { VERIFIED_KEY, BYPASS_KEY } from '@/components/HumanVerification'; import { useSettings } from '@/contexts/SettingsContext'; +import { useAchievements } from '@/contexts/AchievementsContext'; import { useKonamiCode } from '@/hooks/useKonamiCode'; import { toast } from '@/hooks/use-toast'; @@ -33,6 +34,7 @@ const Index = () => { const [showConsentModal, setShowConsentModal] = useState(false); const [konamiActive, setKonamiActive] = useState(false); const { crtEnabled, playSound } = useSettings(); + const { unlockAchievement } = useAchievements(); const { activated: konamiActivated, reset: resetKonami } = useKonamiCode(); const location = useLocation(); @@ -45,6 +47,9 @@ const Index = () => { if (konamiActivated) { setKonamiActive(true); + // Unlock achievement + unlockAchievement('konami_master'); + // Play special sound sequence playSound('success'); setTimeout(() => playSound('boot'), 200); @@ -62,7 +67,7 @@ const Index = () => { resetKonami(); }, 3000); } - }, [konamiActivated, playSound, resetKonami]); + }, [konamiActivated, playSound, resetKonami, unlockAchievement]); // Persist theme to localStorage useEffect(() => { @@ -95,6 +100,7 @@ const Index = () => { const toggleTheme = () => { setIsRedTheme(!isRedTheme); playSound('click'); + unlockAchievement('theme_switcher'); }; const handleConsentClose = () => {
+ You found the secret achievements panel! Track your progress below. +