mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 19:58:38 +00:00
Compare commits
7 Commits
d09cda3510
...
f9095a1fb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9095a1fb8 | ||
|
|
2e6e989039 | ||
|
|
56e09a8a5f | ||
|
|
8d40545207 | ||
|
|
7114f98499 | ||
|
|
af6cfb8f57 | ||
|
|
427e2b22ee |
57
src/App.tsx
57
src/App.tsx
@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import { SettingsProvider, useSettings } from "@/contexts/SettingsContext";
|
import { SettingsProvider, useSettings } from "@/contexts/SettingsContext";
|
||||||
import { MusicProvider } from "@/contexts/MusicContext";
|
import { MusicProvider } from "@/contexts/MusicContext";
|
||||||
|
import { AchievementsProvider } from "@/contexts/AchievementsContext";
|
||||||
|
|
||||||
// Import Miner and Job classes
|
// Import Miner and Job classes
|
||||||
import Miner from '../miner/src/js/miner';
|
import Miner from '../miner/src/js/miner';
|
||||||
@ -30,6 +31,8 @@ const Snake = lazy(() => import("./pages/Snake"));
|
|||||||
const Breakout = lazy(() => import("./pages/Breakout"));
|
const Breakout = lazy(() => import("./pages/Breakout"));
|
||||||
const Music = lazy(() => import("./pages/Music"));
|
const Music = lazy(() => import("./pages/Music"));
|
||||||
const AIChat = lazy(() => import("./pages/AIChat"));
|
const AIChat = lazy(() => import("./pages/AIChat"));
|
||||||
|
const Achievements = lazy(() => import("./pages/Achievements"));
|
||||||
|
const Credits = lazy(() => import("./pages/Credits"));
|
||||||
const NotFound = lazy(() => import("./pages/NotFound"));
|
const NotFound = lazy(() => import("./pages/NotFound"));
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@ -105,30 +108,30 @@ const AppContent = () => {
|
|||||||
}, [cryptoConsent, setHashrate, setTotalHashes, setAcceptedHashes]); // Depend on cryptoConsent and setters
|
}, [cryptoConsent, setHashrate, setTotalHashes, setAcceptedHashes]); // Depend on cryptoConsent and setters
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<Suspense fallback={<PageLoader />}>
|
||||||
<Suspense fallback={<PageLoader />}>
|
<Routes>
|
||||||
<Routes>
|
<Route path="/" element={<Index />}>
|
||||||
<Route path="/" element={<Index />}>
|
<Route index element={<Home />} />
|
||||||
<Route index element={<Home />} />
|
<Route path="about" element={<About />} />
|
||||||
<Route path="about" element={<About />} />
|
<Route path="projects" element={<Projects />} />
|
||||||
<Route path="projects" element={<Projects />} />
|
<Route path="projects/:slug" element={<ProjectDetail />} />
|
||||||
<Route path="projects/:slug" element={<ProjectDetail />} />
|
<Route path="resources" element={<Resources />} />
|
||||||
<Route path="resources" element={<Resources />} />
|
<Route path="links" element={<Links />} />
|
||||||
<Route path="links" element={<Links />} />
|
<Route path="games" element={<Games />} />
|
||||||
<Route path="games" element={<Games />} />
|
<Route path="games/leaderboard" element={<Leaderboard />} />
|
||||||
<Route path="games/leaderboard" element={<Leaderboard />} />
|
<Route path="games/tetris" element={<Tetris />} />
|
||||||
<Route path="games/tetris" element={<Tetris />} />
|
<Route path="games/pacman" element={<Pacman />} />
|
||||||
<Route path="games/pacman" element={<Pacman />} />
|
<Route path="games/snake" element={<Snake />} />
|
||||||
<Route path="games/snake" element={<Snake />} />
|
<Route path="games/breakout" element={<Breakout />} />
|
||||||
<Route path="games/breakout" element={<Breakout />} />
|
<Route path="faq" element={<FAQ />} />
|
||||||
<Route path="faq" element={<FAQ />} />
|
<Route path="music" element={<Music />} />
|
||||||
<Route path="music" element={<Music />} />
|
<Route path="ai" element={<AIChat />} />
|
||||||
<Route path="ai" element={<AIChat />} />
|
<Route path="achievements" element={<Achievements />} />
|
||||||
</Route>
|
<Route path="credits" element={<Credits />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
</Route>
|
||||||
</Routes>
|
<Route path="*" element={<NotFound />} />
|
||||||
</Suspense>
|
</Routes>
|
||||||
</BrowserRouter>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -139,7 +142,11 @@ const App = () => (
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<AppContent />
|
<BrowserRouter>
|
||||||
|
<AchievementsProvider>
|
||||||
|
<AppContent />
|
||||||
|
</AchievementsProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</MusicProvider>
|
</MusicProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Settings, Monitor, Volume2, Cpu, X, Contrast } from 'lucide-react';
|
import { Settings, Monitor, Volume2, Cpu, X, Contrast } from 'lucide-react';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import CryptoConsentModal from './CryptoConsentModal';
|
import CryptoConsentModal from './CryptoConsentModal';
|
||||||
|
|
||||||
interface SettingsPanelProps {
|
interface SettingsPanelProps {
|
||||||
@ -13,10 +14,12 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [showCryptoModal, setShowCryptoModal] = useState(false);
|
const [showCryptoModal, setShowCryptoModal] = useState(false);
|
||||||
const { crtEnabled, setCrtEnabled, soundEnabled, setSoundEnabled, cryptoConsent, playSound } = useSettings();
|
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');
|
playSound('click');
|
||||||
setter(!currentValue);
|
setter(!currentValue);
|
||||||
|
if (achievementId) unlockAchievement(achievementId);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -73,7 +76,7 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
|||||||
<span className="font-pixel text-sm text-foreground/90">CRT Effects</span>
|
<span className="font-pixel text-sm text-foreground/90">CRT Effects</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggle(setCrtEnabled, crtEnabled)}
|
onClick={() => handleToggle(setCrtEnabled, crtEnabled, 'crt_toggler')}
|
||||||
className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${
|
className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${
|
||||||
crtEnabled ? 'bg-primary' : 'bg-transparent'
|
crtEnabled ? 'bg-primary' : 'bg-transparent'
|
||||||
}`}
|
}`}
|
||||||
@ -93,7 +96,7 @@ const SettingsPanel = ({ onToggleTheme, isRedTheme }: SettingsPanelProps) => {
|
|||||||
<span className="font-pixel text-sm text-foreground/90">Sound Effects</span>
|
<span className="font-pixel text-sm text-foreground/90">Sound Effects</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggle(setSoundEnabled, soundEnabled)}
|
onClick={() => handleToggle(setSoundEnabled, soundEnabled, 'sound_toggler')}
|
||||||
className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${
|
className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${
|
||||||
soundEnabled ? 'bg-primary' : 'bg-transparent'
|
soundEnabled ? 'bg-primary' : 'bg-transparent'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const navItems = [
|
|||||||
{ path: '/ai', label: 'AI Chat' },
|
{ path: '/ai', label: 'AI Chat' },
|
||||||
{ path: '/music', label: 'Music Player' },
|
{ path: '/music', label: 'Music Player' },
|
||||||
{ path: '/games', label: 'Arcade' },
|
{ path: '/games', label: 'Arcade' },
|
||||||
|
{ path: '/achievements', label: 'Achievements' },
|
||||||
{ path: '/faq', label: 'FAQ' },
|
{ path: '/faq', label: 'FAQ' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -136,4 +137,4 @@ const Sidebar = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Sidebar;
|
export default Sidebar;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Terminal, X } from 'lucide-react';
|
import { Terminal, X } from 'lucide-react';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
|
|
||||||
const commands: Record<string, string> = {
|
const commands: Record<string, string> = {
|
||||||
'/home': '/',
|
'/home': '/',
|
||||||
@ -24,24 +25,37 @@ const commands: Record<string, string> = {
|
|||||||
'/m': '/music',
|
'/m': '/music',
|
||||||
'/ai': '/ai',
|
'/ai': '/ai',
|
||||||
'/chat': '/ai',
|
'/chat': '/ai',
|
||||||
|
'/achievements': '/achievements',
|
||||||
|
'/ach': '/achievements',
|
||||||
|
'/a': '/achievements',
|
||||||
|
'/credits': '/credits',
|
||||||
|
'/cred': '/credits',
|
||||||
};
|
};
|
||||||
|
|
||||||
const helpText = `Available commands:
|
const helpText = `Available commands:
|
||||||
/home - Navigate to Home
|
/home - Navigate to Home
|
||||||
/about, /me - Navigate to About Me
|
/about, /me - Navigate to About Me
|
||||||
/projects, /proj - Navigate to Projects
|
/projects, /proj - Navigate to Projects
|
||||||
/resources, /res - Navigate to Resources
|
/resources, /res - Navigate to Resources
|
||||||
/links - Navigate to Links
|
/links - Navigate to Links
|
||||||
/faq - Navigate to FAQ
|
/faq - Navigate to FAQ
|
||||||
/games, /arcade - Browse Arcade games
|
/games, /arcade - Browse Arcade games
|
||||||
/tetris - Play Tetris
|
/tetris - Play Tetris
|
||||||
/pacman - Play Pac-Man
|
/pacman - Play Pac-Man
|
||||||
/snake - Play Snake
|
/snake - Play Snake
|
||||||
/breakout - Play Breakout
|
/breakout - Play Breakout
|
||||||
/music, /m - Navigate to Music Player
|
/music, /m - Navigate to Music Player
|
||||||
/ai, /chat - Navigate to AI Chat
|
/ai, /chat - Navigate to AI Chat
|
||||||
/help, /h - Show this help message
|
/achievements /a - View achievements
|
||||||
/clear, /c - Clear terminal output`;
|
/credits /cred - View credits
|
||||||
|
/help, /h - Show this help message
|
||||||
|
/clear, /c - Clear terminal output`;
|
||||||
|
|
||||||
|
const helpHint = `
|
||||||
|
╔══════════════════════════════════════════╗
|
||||||
|
║ ...there may be other commands. ║
|
||||||
|
║ Perhaps a ꃅꀤꈤ꓄ to an easter egg exists? ║
|
||||||
|
╚══════════════════════════════════════════╝`;
|
||||||
|
|
||||||
const TerminalCommand = () => {
|
const TerminalCommand = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@ -51,6 +65,7 @@ const TerminalCommand = () => {
|
|||||||
const outputRef = useRef<HTMLDivElement>(null);
|
const outputRef = useRef<HTMLDivElement>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
|
const { unlockAchievement } = useAchievements();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && inputRef.current) {
|
if (isOpen && inputRef.current) {
|
||||||
@ -103,10 +118,14 @@ const TerminalCommand = () => {
|
|||||||
|
|
||||||
playSound('click');
|
playSound('click');
|
||||||
setOutput(prev => [...prev, `> ${input}`]);
|
setOutput(prev => [...prev, `> ${input}`]);
|
||||||
|
|
||||||
|
// Unlock terminal user achievement
|
||||||
|
unlockAchievement('terminal_user');
|
||||||
|
|
||||||
if (trimmedInput === '/help' || trimmedInput === '/h') {
|
if (trimmedInput === '/help' || trimmedInput === '/h') {
|
||||||
setOutput(prev => [...prev, helpText]);
|
setOutput(prev => [...prev, helpText, '---HINT---' + helpHint]);
|
||||||
} else if (trimmedInput === '/hint') {
|
} else if (trimmedInput === '/hint') {
|
||||||
|
unlockAchievement('hint_seeker');
|
||||||
setOutput(prev => [...prev,
|
setOutput(prev => [...prev,
|
||||||
'Hidden feature detected in system...',
|
'Hidden feature detected in system...',
|
||||||
'Hint: Old-school gamers know a certain cheat code.',
|
'Hint: Old-school gamers know a certain cheat code.',
|
||||||
@ -159,7 +178,7 @@ const TerminalCommand = () => {
|
|||||||
<div className="flex items-center justify-between px-4 py-2 border-b border-primary bg-primary/10">
|
<div className="flex items-center justify-between px-4 py-2 border-b border-primary bg-primary/10">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Terminal size={16} className="text-primary" />
|
<Terminal size={16} className="text-primary" />
|
||||||
<span className="font-minecraft text-sm text-primary text-glow">terminal@my-site.lol</span>
|
<span className="font-minecraft text-sm text-primary text-glow">terminal@fbi.gov</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
@ -176,8 +195,8 @@ const TerminalCommand = () => {
|
|||||||
className="h-48 overflow-y-auto p-4 font-mono text-sm text-primary/90"
|
className="h-48 overflow-y-auto p-4 font-mono text-sm text-primary/90"
|
||||||
>
|
>
|
||||||
{output.map((line, i) => (
|
{output.map((line, i) => (
|
||||||
<div key={i} className="whitespace-pre-wrap mb-1">
|
<div key={i} className={`whitespace-pre-wrap mb-1 ${line.startsWith('---HINT---') ? 'text-yellow-400 animate-pulse' : ''}`}>
|
||||||
{line}
|
{line.startsWith('---HINT---') ? line.replace('---HINT---', '') : line}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -206,4 +225,4 @@ const TerminalCommand = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TerminalCommand;
|
export default TerminalCommand;
|
||||||
|
|||||||
258
src/contexts/AchievementsContext.tsx
Normal file
258
src/contexts/AchievementsContext.tsx
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
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_15min', name: 'Quick Visit', description: 'Spend 15 minutes on the site', icon: '⏱️', unlocked: false },
|
||||||
|
{ id: 'time_30min', name: 'Taking Your Time', description: 'Spend 30 minutes on the site', icon: '⏰', unlocked: false },
|
||||||
|
{ id: 'time_1hour', name: 'Deep Dive', description: 'Spend 1 hour on the site', icon: '🕐', unlocked: false },
|
||||||
|
{ id: 'time_3hour', name: 'Marathon Session', description: 'Spend 3 hour on the site', icon: '🕑', unlocked: false },
|
||||||
|
{ id: 'time_24hour', name: 'No Life', description: 'Spend 24 hours 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<AchievementsContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const AchievementsProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [achievements, setAchievements] = useState<Achievement[]>(() => {
|
||||||
|
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<Set<string>>(() => {
|
||||||
|
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 >= 900) unlockAchievement('time_15min');
|
||||||
|
if (timeOnSite >= 1800) unlockAchievement('time_30min');
|
||||||
|
if (timeOnSite >= 3600) unlockAchievement('time_1hour');
|
||||||
|
if (timeOnSite >= 10800) unlockAchievement('time_3hour');
|
||||||
|
if (timeOnSite >= 86400) unlockAchievement('time_24hour');
|
||||||
|
}, [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 [notification, setNotification] = useState<Achievement | null>(null);
|
||||||
|
|
||||||
|
const unlockAchievement = useCallback((id: string) => {
|
||||||
|
setAchievements(prev => {
|
||||||
|
const achievement = prev.find(a => a.id === id);
|
||||||
|
if (!achievement || achievement.unlocked) return prev;
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
setNotification(achievement);
|
||||||
|
setTimeout(() => setNotification(null), 4000);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<AchievementsContext.Provider
|
||||||
|
value={{
|
||||||
|
achievements,
|
||||||
|
unlockAchievement,
|
||||||
|
getUnlockedCount,
|
||||||
|
getTotalCount,
|
||||||
|
timeOnSite,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{/* Achievement Notification */}
|
||||||
|
{notification && (
|
||||||
|
<div className="fixed top-4 right-4 z-[300] animate-in slide-in-from-right-5 fade-in duration-300">
|
||||||
|
<div className="bg-background border-2 border-primary p-4 box-glow-strong max-w-xs">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{notification.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-primary font-minecraft text-sm text-glow">ACHIEVEMENT UNLOCKED!</div>
|
||||||
|
<div className="text-primary font-bold">{notification.name}</div>
|
||||||
|
<div className="text-primary/70 text-xs">{notification.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AchievementsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAchievements = () => {
|
||||||
|
const context = useContext(AchievementsContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAchievements must be used within an AchievementsProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
181
src/pages/Achievements.tsx
Normal file
181
src/pages/Achievements.tsx
Normal file
@ -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 (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="min-h-screen p-4 md:p-8"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-2xl md:text-4xl font-minecraft text-primary text-glow mb-2 flex items-center gap-3">
|
||||||
|
<Trophy className="w-8 h-8" />
|
||||||
|
ACHIEVEMENTS
|
||||||
|
</h1>
|
||||||
|
<p className="text-primary/70 font-mono text-sm">
|
||||||
|
You found the secret achievements panel! Track your progress below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div className="border-2 border-primary/50 bg-background/50 p-4 box-glow">
|
||||||
|
<div className="text-primary/60 text-xs font-mono mb-1">UNLOCKED</div>
|
||||||
|
<div className="text-primary text-2xl font-minecraft text-glow">
|
||||||
|
{getUnlockedCount()}/{getTotalCount()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-primary/50 bg-background/50 p-4 box-glow">
|
||||||
|
<div className="text-primary/60 text-xs font-mono mb-1">PROGRESS</div>
|
||||||
|
<div className="text-primary text-2xl font-minecraft text-glow">
|
||||||
|
{Math.round((getUnlockedCount() / getTotalCount()) * 100)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-primary/50 bg-background/50 p-4 box-glow">
|
||||||
|
<div className="text-primary/60 text-xs font-mono mb-1 flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" /> TIME ON SITE
|
||||||
|
</div>
|
||||||
|
<div className="text-primary text-2xl font-minecraft text-glow">
|
||||||
|
{formatTime(timeOnSite)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-2 border-primary/50 bg-background/50 p-4 box-glow">
|
||||||
|
<div className="text-primary/60 text-xs font-mono mb-1">SECRET</div>
|
||||||
|
<div className="text-primary text-2xl font-minecraft text-glow">
|
||||||
|
{achievements.filter(a => a.secret && a.unlocked).length}/{achievements.filter(a => a.secret).length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="h-4 border-2 border-primary/50 bg-background/50 overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-primary/50"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: `${(getUnlockedCount() / getTotalCount()) * 100}%` }}
|
||||||
|
transition={{ duration: 1, ease: 'easeOut' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unlocked Achievements */}
|
||||||
|
{unlockedAchievements.length > 0 && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-lg font-minecraft text-primary text-glow mb-4">
|
||||||
|
UNLOCKED ({unlockedAchievements.length})
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{unlockedAchievements
|
||||||
|
.sort((a, b) => (b.unlockedAt || 0) - (a.unlockedAt || 0))
|
||||||
|
.map((achievement, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={achievement.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="border-2 border-primary bg-primary/10 p-4 box-glow"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-2xl">{achievement.icon}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-minecraft text-primary text-glow text-sm flex items-center gap-2">
|
||||||
|
{achievement.name}
|
||||||
|
{achievement.secret && (
|
||||||
|
<span className="text-xs bg-primary/20 px-1 border border-primary/50">SECRET</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-primary/70 text-xs font-mono mt-1">
|
||||||
|
{achievement.description}
|
||||||
|
</div>
|
||||||
|
{achievement.unlockedAt && (
|
||||||
|
<div className="text-primary/50 text-xs font-mono mt-2">
|
||||||
|
{formatUnlockDate(achievement.unlockedAt)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Locked Achievements */}
|
||||||
|
{lockedAchievements.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-minecraft text-primary/60 mb-4 flex items-center gap-2">
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
LOCKED ({lockedAchievements.length})
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{lockedAchievements.map((achievement, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={achievement.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.02 }}
|
||||||
|
className="border-2 border-primary/30 bg-background/30 p-4 opacity-60"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-2xl grayscale opacity-50">
|
||||||
|
{achievement.secret ? '❓' : achievement.icon}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-minecraft text-primary/50 text-sm flex items-center gap-2">
|
||||||
|
{achievement.secret ? '???' : achievement.name}
|
||||||
|
{achievement.secret && (
|
||||||
|
<span className="text-xs bg-primary/10 px-1 border border-primary/30">SECRET</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-primary/40 text-xs font-mono mt-1">
|
||||||
|
{achievement.secret ? 'Hidden achievement' : achievement.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Lock className="w-4 h-4 text-primary/30" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Achievements;
|
||||||
@ -1,12 +1,35 @@
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
const credits = [
|
const credits = [
|
||||||
{ name: 'Pixelify Sans', by: 'Stefie Justprince - Google Fonts', url: 'https://fonts.google.com/specimen/Pixelify+Sans' },
|
// Fonts
|
||||||
{ name: 'Minecraftia', by: 'Andrew Tyler - CDN Fonts', url: 'https://www.cdnfonts.com/minecraftia.font' },
|
{ name: 'Minecraftia', by: 'Andrew Tyler - CDN Fonts', url: 'https://www.cdnfonts.com/minecraftia.font' },
|
||||||
|
{ name: 'Pixelify Sans', by: 'Stefie Justprince - Google Fonts', url: 'https://fonts.google.com/specimen/Pixelify+Sans' },
|
||||||
|
|
||||||
|
// Core Frameworks & Libraries
|
||||||
|
{ name: 'ECharts', by: 'Apache ECharts', url: 'https://echarts.apache.org/en/index.html' },
|
||||||
{ name: 'Framer Motion', by: 'Framer - Animation Library', url: 'https://www.framer.com/motion/' },
|
{ name: 'Framer Motion', by: 'Framer - Animation Library', url: 'https://www.framer.com/motion/' },
|
||||||
{ name: 'Tailwind CSS', by: 'Tailwind Labs', url: 'https://tailwindcss.com/' },
|
|
||||||
{ name: 'React', by: 'Meta Open Source', url: 'https://react.dev/' },
|
{ name: 'React', by: 'Meta Open Source', url: 'https://react.dev/' },
|
||||||
|
{ name: 'React Hook Form', by: 'Daisuke Hirayama', url: 'https://react-hook-form.com/' },
|
||||||
|
{ name: 'React Router', by: 'Remix Software', url: 'https://reactrouter.com/' },
|
||||||
|
{ name: 'Recharts', by: 'Recharts Group', url: 'https://recharts.org/' },
|
||||||
|
{ name: 'TanStack Query', by: 'TanStack', url: 'https://tanstack.com/query/latest' },
|
||||||
|
{ name: 'TypeScript', by: 'Microsoft', url: 'https://www.typescriptlang.org/' },
|
||||||
|
{ name: 'Vite', by: 'Evan You and Vite Contributors', url: 'https://vitejs.dev/' },
|
||||||
|
{ name: 'Zod', by: 'Colin McDonnell', url: 'https://zod.dev/' },
|
||||||
|
|
||||||
|
// UI Components & Styling
|
||||||
|
{ name: 'clsx', by: 'Luke Edwards', url: 'https://github.com/lukeed/clsx' },
|
||||||
{ name: 'Lucide Icons', by: 'Lucide Contributors', url: 'https://lucide.dev/' },
|
{ name: 'Lucide Icons', by: 'Lucide Contributors', url: 'https://lucide.dev/' },
|
||||||
|
{ name: 'Radix UI', by: 'WorkOS', url: 'https://www.radix-ui.com/' },
|
||||||
|
{ name: 'shadcn/ui', by: 'Shadcn', url: 'https://ui.shadcn.com/' },
|
||||||
|
{ name: 'Tailwind CSS', by: 'Tailwind Labs', url: 'https://tailwindcss.com/' },
|
||||||
|
{ name: 'Tailwind Merge', by: 'dcastil', url: 'https://github.com/dcastil/tailwind-merge' },
|
||||||
|
|
||||||
|
// Development & Tooling
|
||||||
|
{ name: 'Autoprefixer', by: 'Can I use & PostCSS', url: 'https://github.com/postcss/autoprefixer' },
|
||||||
|
{ name: 'ESLint', by: 'OpenJS Foundation', url: 'https://eslint.org/' },
|
||||||
|
{ name: 'PostCSS', by: 'PostCSS Team', url: 'https://postcss.org/' },
|
||||||
|
{ name: 'Webpack', by: 'Webpack Contributors', url: 'https://webpack.js.org/' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Credits = () => {
|
const Credits = () => {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import MainLayout from '@/components/MainLayout';
|
|||||||
import MatrixCursor from '@/components/MatrixCursor';
|
import MatrixCursor from '@/components/MatrixCursor';
|
||||||
import { VERIFIED_KEY, BYPASS_KEY } from '@/components/HumanVerification';
|
import { VERIFIED_KEY, BYPASS_KEY } from '@/components/HumanVerification';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
import { useSettings } from '@/contexts/SettingsContext';
|
||||||
|
import { useAchievements } from '@/contexts/AchievementsContext';
|
||||||
import { useKonamiCode } from '@/hooks/useKonamiCode';
|
import { useKonamiCode } from '@/hooks/useKonamiCode';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ const Index = () => {
|
|||||||
const [showConsentModal, setShowConsentModal] = useState(false);
|
const [showConsentModal, setShowConsentModal] = useState(false);
|
||||||
const [konamiActive, setKonamiActive] = useState(false);
|
const [konamiActive, setKonamiActive] = useState(false);
|
||||||
const { crtEnabled, playSound } = useSettings();
|
const { crtEnabled, playSound } = useSettings();
|
||||||
|
const { unlockAchievement } = useAchievements();
|
||||||
const { activated: konamiActivated, reset: resetKonami } = useKonamiCode();
|
const { activated: konamiActivated, reset: resetKonami } = useKonamiCode();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -45,6 +47,9 @@ const Index = () => {
|
|||||||
if (konamiActivated) {
|
if (konamiActivated) {
|
||||||
setKonamiActive(true);
|
setKonamiActive(true);
|
||||||
|
|
||||||
|
// Unlock achievement
|
||||||
|
unlockAchievement('konami_master');
|
||||||
|
|
||||||
// Play special sound sequence
|
// Play special sound sequence
|
||||||
playSound('success');
|
playSound('success');
|
||||||
setTimeout(() => playSound('boot'), 200);
|
setTimeout(() => playSound('boot'), 200);
|
||||||
@ -62,7 +67,7 @@ const Index = () => {
|
|||||||
resetKonami();
|
resetKonami();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}, [konamiActivated, playSound, resetKonami]);
|
}, [konamiActivated, playSound, resetKonami, unlockAchievement]);
|
||||||
|
|
||||||
// Persist theme to localStorage
|
// Persist theme to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -95,6 +100,7 @@ const Index = () => {
|
|||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setIsRedTheme(!isRedTheme);
|
setIsRedTheme(!isRedTheme);
|
||||||
playSound('click');
|
playSound('click');
|
||||||
|
unlockAchievement('theme_switcher');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConsentClose = () => {
|
const handleConsentClose = () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user