Formating and small updates

This commit is contained in:
End 2025-09-19 08:52:17 -07:00
parent 9ae7b63d2d
commit 8c877c2358
52 changed files with 7433 additions and 2968 deletions

View file

@ -61,20 +61,7 @@ npm start
The application will be available at http://localhost:3000 The application will be available at http://localhost:3000
### Component Structure
The project follows a modular component structure:
```
src/
├── components/
│ ├── LastFMTrack/ # Music integration
│ ├── GithubRepos/ # GitHub repository display
│ ├── LoadingFox/ # Loading states
│ └── ParallaxEffect/ # Visual effects
├── App.tsx # Main application component
└── index.tsx # Application entry point
```
## 🌐 Deployment ## 🌐 Deployment

View file

@ -1,28 +1,39 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EndofTimee</title>
<meta name="description" content="EndofTimee's personal site ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
<meta property="og:title" content="EndofTimee ProtoFox Personal Site" />
<meta property="og:description" content="EndofTimee's personal site ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://endoftimee.com/" />
<meta property="og:image" content="/logo.svg" />
<meta property="og:image:alt" content="Fox Logo" />
<meta property="og:site_name" content="EndofTimee" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="EndofTimee ProtoFox Personal Site" />
<meta name="twitter:description" content="EndofTimee's personal site ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
<meta name="twitter:image" content="/logo.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EndofTimee</title>
<meta
name="description"
content="EndofTimee's personal site developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
/>
<meta property="og:title" content="EndofTimee Personal Site" />
<meta
property="og:description"
content="EndofTimee's personal site developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://endoftimee.com/" />
<meta property="og:image" content="/logo.svg" />
<meta property="og:image:alt" content="Fox Logo" />
<meta property="og:site_name" content="EndofTimee" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="EndofTimee Personal Site" />
<meta
name="twitter:description"
content="EndofTimee's personal site Developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
/>
<meta name="twitter:image" content="/logo.svg" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

4637
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"eslint-config-react-app": "7.0.1",
"itty-router": "^4.0.27", "itty-router": "^4.0.27",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -39,14 +40,16 @@
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "^1.2.2", "@vitest/ui": "^1.2.2",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "6.0.0-rc.2",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"husky": "^9.0.10", "husky": "^9.0.10",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"lucide-react": "0.474.0", "lucide-react": "0.474.0",
"plugin-react": "0.0.1-security",
"postcss": "8.5.1", "postcss": "8.5.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
@ -54,6 +57,7 @@
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "6.2.2", "vite": "6.2.2",
"vite-plugin-babel": "1.3.2",
"vitest": "3.0.9", "vitest": "3.0.9",
"web-vitals": "4.2.4", "web-vitals": "4.2.4",
"wrangler": "^3.28.0" "wrangler": "^3.28.0"

View file

@ -4,113 +4,120 @@ import { AuthProvider, useAuth } from "@/context/AuthContext";
import Navbar from "@/components/Navbar"; import Navbar from "@/components/Navbar";
import AboutPage from "@/pages/AboutPage"; import AboutPage from "@/pages/AboutPage";
import ProjectsPage from "@/pages/ProjectsPage"; import ProjectsPage from "@/pages/ProjectsPage";
import APCSPPage from "@/pages/APCSPPage"; // import APCSPPage from "@/pages/APCSPPage";
// import LoginPage from "@/pages/LoginPage"; // import LoginPage from "@/pages/LoginPage";
// import SystemPage from "@/pages/SystemPage"; // import SystemPage from "@/pages/SystemPage";
import VNCViewer from "@/components/VNCViewer";
// import SystemStatus from "@/components/SystemStatus"; // import SystemStatus from "@/components/SystemStatus";
// import SwitchNotification from "@/components/SwitchNotification"; // import SwitchNotification from "@/components/SwitchNotification";
// import ProtectedRoute from "@/components/ProtectedRoute"; // import ProtectedRoute from "@/components/ProtectedRoute";
import FoxGame from "@/games/fox-adventure/components/FoxGame"; import FoxGame from "@/games/fox-adventure/components/FoxGame";
import ThemeToggle from "@/components/ThemeToggle"; import ThemeToggle from "@/components/ThemeToggle";
import EndOSBootAnimation from "@/components/EndOSBootAnimation"; import EndOSBootAnimation from "@/components/EndOSBootAnimation";
import '@/styles/animations.css'; import "@/styles/animations.css";
import '@/styles/protofox-theme.css'; import "@/styles/protofox-theme.css";
// EndOS animation control // EndOS animation control
const useEndOSAnimation = () => { const useEndOSAnimation = () => {
const [bootComplete, setBootComplete] = useState(false); const [bootComplete, setBootComplete] = useState(false);
// Using underscore prefix to indicate intentionally unused variable // Using underscore prefix to indicate intentionally unused variable
const [skipBoot, _setSkipBoot] = useState(() => { const [skipBoot, _setSkipBoot] = useState(() => {
// Check for URL parameter that allows skipping the boot animation // Check for URL parameter that allows skipping the boot animation
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const skipParam = urlParams.get('skipBoot'); const skipParam = urlParams.get("skipBoot");
// Also check if user has already seen the animation in this session // Also check if user has already seen the animation in this session
const sessionSeen = sessionStorage.getItem('endos-boot-complete') === 'true'; const sessionSeen =
sessionStorage.getItem("endos-boot-complete") === "true";
return skipParam === 'true' || sessionSeen;
}); return skipParam === "true" || sessionSeen;
});
// Handle boot animation completion
const handleBootComplete = () => { // Handle boot animation completion
setBootComplete(true); const handleBootComplete = () => {
sessionStorage.setItem('endos-boot-complete', 'true'); setBootComplete(true);
}; sessionStorage.setItem("endos-boot-complete", "true");
};
return { bootComplete, skipBoot, handleBootComplete };
return { bootComplete, skipBoot, handleBootComplete };
}; };
// AuthChecker component to access auth context inside the router // AuthChecker component to access auth context inside the router
const AuthChecker = ({ children }: { children: React.ReactNode }) => { const AuthChecker = ({ children }: { children: React.ReactNode }) => {
const auth = useAuth(); const auth = useAuth();
const [isStatusVisible, setIsStatusVisible] = useState(false); const [isStatusVisible, setIsStatusVisible] = useState(false);
// Using underscore prefix for all unused state variables // Using underscore prefix for all unused state variables
const [_showNotification, setShowNotification] = useState(false); const [_showNotification, setShowNotification] = useState(false);
const [_notificationType, setNotificationType] = useState<'switch' | 'warning' | 'notice'>('switch'); const [_notificationType, setNotificationType] = useState<
const [_notificationMessage, setNotificationMessage] = useState(''); "switch" | "warning" | "notice"
const [_selectedAlter, setSelectedAlter] = useState(''); >("switch");
const [_notificationMessage, setNotificationMessage] = useState("");
const [_selectedAlter, setSelectedAlter] = useState("");
// Toggle system status floating panel // Toggle system status floating panel
const toggleStatus = () => { const toggleStatus = () => {
setIsStatusVisible(prev => !prev); setIsStatusVisible((prev) => !prev);
}; };
// Simulate random switches for demo purposes // Simulate random switches for demo purposes
useEffect(() => { useEffect(() => {
if (!auth.isAuthenticated) return; if (!auth.isAuthenticated) return;
// Every 5-15 minutes, show a switch notification // Every 5-15 minutes, show a switch notification
const randomInterval = Math.floor(Math.random() * (15 - 5 + 1) + 5) * 60 * 1000; const randomInterval =
const interval = setInterval(() => { Math.floor(Math.random() * (15 - 5 + 1) + 5) * 60 * 1000;
// 70% chance of switch, 20% chance of notice, 10% chance of warning const interval = setInterval(() => {
const rand = Math.random(); // 70% chance of switch, 20% chance of notice, 10% chance of warning
const rand = Math.random();
if (rand < 0.7) {
setNotificationType('switch');
setSelectedAlter(''); // Random alter will be selected
} else if (rand < 0.9) {
setNotificationType('notice');
setNotificationMessage('System communication active');
} else {
setNotificationType('warning');
setNotificationMessage('System experiencing stress');
}
setShowNotification(true);
}, randomInterval);
return () => clearInterval(interval); if (rand < 0.7) {
}, [auth.isAuthenticated]); setNotificationType("switch");
setSelectedAlter(""); // Random alter will be selected
} else if (rand < 0.9) {
setNotificationType("notice");
setNotificationMessage("System communication active");
} else {
setNotificationType("warning");
setNotificationMessage("System experiencing stress");
}
return ( setShowNotification(true);
<> }, randomInterval);
{children}
return () => clearInterval(interval);
{/* Floating System Status for authenticated users */} }, [auth.isAuthenticated]);
{auth.isAuthenticated && (
return (
<> <>
<div {children}
className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
isStatusVisible ? 'translate-y-0' : 'translate-y-[calc(100%-40px)]' {/* Floating System Status for authenticated users */}
}`} {auth.isAuthenticated && (
> <>
<div <div
className="p-2 bg-background-secondary rounded-t-lg cursor-pointer flex justify-center items-center" className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
onClick={toggleStatus} isStatusVisible
> ? "translate-y-0"
<span className="text-xs font-medium"> : "translate-y-[calc(100%-40px)]"
{isStatusVisible ? "Hide System Status" : "System Status"} }`}
</span> >
</div> <div
{/* <SystemStatus className="p-2 bg-background-secondary rounded-t-lg cursor-pointer flex justify-center items-center"
onClick={toggleStatus}
>
<span className="text-xs font-medium">
{isStatusVisible
? "Hide System Status"
: "System Status"}
</span>
</div>
{/* <SystemStatus
minimal={true} minimal={true}
className="shadow-lg rounded-t-none w-[300px] max-w-[calc(100vw-2rem)]" className="shadow-lg rounded-t-none w-[300px] max-w-[calc(100vw-2rem)]"
/> */} /> */}
</div> </div>
{/* System Notifications */} {/* System Notifications */}
{/* <SwitchNotification {/* <SwitchNotification
show={showNotification} show={showNotification}
onClose={() => setShowNotification(false)} onClose={() => setShowNotification(false)}
alterName={selectedAlter} alterName={selectedAlter}
@ -119,10 +126,10 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
autoClose autoClose
autoCloseDelay={5000} autoCloseDelay={5000}
/> */} /> */}
</>
)}
</> </>
)} );
</>
);
}; };
const App = () => { const App = () => {
@ -132,26 +139,31 @@ const App = () => {
// Demo the switch notification after a delay // Demo the switch notification after a delay
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
setShowInitialSwitchDemo(true); setShowInitialSwitchDemo(true);
// Hide after 5 seconds // Hide after 5 seconds
setTimeout(() => { setTimeout(() => {
setShowInitialSwitchDemo(false); setShowInitialSwitchDemo(false);
}, 5000); }, 5000);
}, 10000); }, 10000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
useEffect(() => { useEffect(() => {
// Konami code sequence // Konami code sequence
const konamiCode = [ const konamiCode = [
'ArrowUp', 'ArrowUp', "ArrowUp",
'ArrowDown', 'ArrowDown', "ArrowUp",
'ArrowLeft', 'ArrowRight', "ArrowDown",
'ArrowLeft', 'ArrowRight', "ArrowDown",
'b', 'a' "ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
]; ];
let index = 0; let index = 0;
@ -159,12 +171,12 @@ const App = () => {
// Check if the pressed key matches the next key in the sequence // Check if the pressed key matches the next key in the sequence
if (event.key === konamiCode[index]) { if (event.key === konamiCode[index]) {
index++; index++;
// If the entire sequence is completed // If the entire sequence is completed
if (index === konamiCode.length) { if (index === konamiCode.length) {
setIsGameActive(true); setIsGameActive(true);
// Optional: Play a sound or show a notification // Optional: Play a sound or show a notification
console.log('Konami code activated!'); console.log("Konami code activated!");
} }
} else { } else {
// Reset if a wrong key is pressed // Reset if a wrong key is pressed
@ -172,55 +184,64 @@ const App = () => {
} }
}; };
window.addEventListener('keydown', handleKeydown); window.addEventListener("keydown", handleKeydown);
// Clean up the event listener on component unmount // Clean up the event listener on component unmount
return () => window.removeEventListener('keydown', handleKeydown); return () => window.removeEventListener("keydown", handleKeydown);
}, []); }, []);
return ( return (
<AuthProvider> <AuthProvider>
{/* EndOS Boot Animation */} {/* EndOS Boot Animation */}
{!skipBoot && !bootComplete && ( {!skipBoot && !bootComplete && (
<EndOSBootAnimation onComplete={handleBootComplete} /> <EndOSBootAnimation onComplete={handleBootComplete} />
)} )}
{/* Main Application - Only visible after boot animation completes */} {/* Main Application - Only visible after boot animation completes */}
<div style={{ <div
visibility: bootComplete || skipBoot ? 'visible' : 'hidden', style={{
opacity: bootComplete || skipBoot ? 1 : 0, visibility: bootComplete || skipBoot ? "visible" : "hidden",
transition: 'opacity 0.5s ease-in-out' opacity: bootComplete || skipBoot ? 1 : 0,
}}> transition: "opacity 0.5s ease-in-out",
<Router> }}
<AuthChecker> >
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}> <Router>
{/* Background Logo */} <AuthChecker>
<div className="fixed inset-0 z-behind pointer-events-none"> <div
<div className="absolute inset-0"> className={`min-h-screen bg-background-primary ${isGameActive ? "game-active" : ""}`}
<img >
src="/logo.jpg" {/* Background Logo */}
alt="Background Logo" <div className="fixed inset-0 z-behind pointer-events-none">
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]" <div className="absolute inset-0">
/> <img
</div> src="/logo.jpg"
</div> alt="Background Logo"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]"
{/* Main Content */} />
<div className="relative"> </div>
<Navbar /> </div>
{/* Theme Toggle */} {/* Main Content */}
<div className="fixed top-20 right-4 z-30"> <div className="relative">
<ThemeToggle /> <Navbar />
</div>
{/* Theme Toggle */}
<main className="content-wrapper section-spacing pb-20 animate-fade-in"> <div className="fixed top-20 right-4 z-30">
<Routes> <ThemeToggle />
<Route path="/" element={<AboutPage />} /> </div>
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/apcsp" element={<APCSPPage />} /> <main className="content-wrapper section-spacing pb-20 animate-fade-in">
<Route path="/vnc" element={<VNCViewer />} /> <Routes>
{/* <Route path="/login" element={<LoginPage />} /> <Route
path="/"
element={<AboutPage />}
/>
<Route
path="/projects"
element={<ProjectsPage />}
/>
{/* <Route path="/apcsp" element={<APCSPPage />} /> */}
{/* <Route path="/login" element={<LoginPage />} />
<Route <Route
path="/system" path="/system"
element={ element={
@ -229,46 +250,58 @@ const App = () => {
</ProtectedRoute> </ProtectedRoute>
} }
/> */} /> */}
<Route path="*" element={ <Route
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4"> path="*"
<h1 className="text-4xl font-bold text-glow">404: Page Not Found</h1> element={
<p className="text-xl text-text-primary/80">This fox couldn't find what you're looking for.</p> <div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
</div> <h1 className="text-4xl font-bold text-glow">
} /> 404: Page Not Found
</Routes> </h1>
</main> <p className="text-xl text-text-primary/80">
This fox couldn't find
{/* Footer */} what you're looking for.
<footer className="py-6 border-t border-accent-primary/10 text-center text-sm text-text-primary/60"> </p>
<p>© 2023 - {new Date().getFullYear()} EndofTimee. All rights reserved.</p> </div>
<div className="flex justify-center items-center gap-2 mt-2"> }
<span className="text-xs">Try the Konami code: BA</span> />
<div className="bg-background-secondary px-2 py-0.5 rounded-full text-[10px] text-accent-primary"> </Routes>
v1.3.0 </main>
</div>
</div>
</footer>
</div>
{/* Footer */}
<footer className="py-6 border-t border-accent-primary/10 text-center text-sm text-text-primary/60">
<p>
© 2023 - {new Date().getFullYear()}{" "}
EndofTimee. All rights reserved.
</p>
<div className="flex justify-center items-center gap-2 mt-2">
<span className="text-xs">
Try the Konami code: BA
</span>
<div className="bg-background-secondary px-2 py-0.5 rounded-full text-[10px] text-accent-primary">
v0.9.5
</div>
</div>
</footer>
</div>
{/* Fox Game Overlay - Activated by Konami Code */} {/* Fox Game Overlay - Activated by Konami Code */}
{isGameActive && ( {isGameActive && (
<> <>
<FoxGame /> <FoxGame />
<button <button
onClick={() => setIsGameActive(false)} onClick={() => setIsGameActive(false)}
className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors" className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors"
> >
Exit Game Exit Game
</button> </button>
</> </>
)} )}
</div> </div>
</AuthChecker> </AuthChecker>
</Router> </Router>
</div> </div>
</AuthProvider> </AuthProvider>
); );
}; };
export default App; export default App;

View file

@ -1,215 +1,269 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import '@/styles/EndOSBootAnimation.css'; import "@/styles/EndOSBootAnimation.css";
interface EndOSBootAnimationProps { interface EndOSBootAnimationProps {
// Using underscores to mark unused props to avoid TypeScript errors // Using underscores to mark unused props to avoid TypeScript errors
_customLogo?: string; _customLogo?: string;
_customColors?: { _customColors?: {
primary?: string; primary?: string;
secondary?: string; secondary?: string;
fox?: string; fox?: string;
}; };
onComplete?: () => void; onComplete?: () => void;
skipAnimation?: boolean; skipAnimation?: boolean;
} }
const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
onComplete, onComplete,
skipAnimation = false, skipAnimation = false,
// Rename with underscores to indicate variables are intentionally unused // Rename with underscores to indicate variables are intentionally unused
_customLogo, _customLogo,
_customColors _customColors,
}) => { }) => {
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const [bootStage, setBootStage] = useState(0); const [bootStage, setBootStage] = useState(0);
const [showLogo, setShowLogo] = useState(false); const [showLogo, setShowLogo] = useState(false);
const [bootComplete, setBootComplete] = useState(false); const [bootComplete, setBootComplete] = useState(false);
// Boot sequence timing // Boot sequence timing
useEffect(() => { useEffect(() => {
if (skipAnimation) { if (skipAnimation) {
handleAnimationComplete(); handleAnimationComplete();
return; return;
} }
// Initialize boot sequence with proper timing to prevent overlap // Initialize boot sequence with proper timing to prevent overlap
const bootSequence = [ const bootSequence = [
{ stage: 1, delay: 1500 }, // Initial screen flicker { stage: 1, delay: 1500 }, // Initial screen flicker
{ stage: 2, delay: 3000 }, // BIOS check (longer time to read) { stage: 2, delay: 3000 }, // BIOS check (longer time to read)
{ stage: 3, delay: 3500 }, // System scan (longer for progress bar) { stage: 3, delay: 3500 }, // System scan (longer for progress bar)
{ stage: 4, delay: 3500 }, // Loading modules (allow time for animation) { stage: 4, delay: 3500 }, // Loading modules (allow time for animation)
{ stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits) { stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits)
{ stage: 6, delay: 3000 }, // Show logo (allow time to appreciate) { stage: 6, delay: 3000 }, // Show logo (allow time to appreciate)
{ stage: 7, delay: 3000 }, // Final activation (longer read time) { stage: 7, delay: 3000 }, // Final activation (longer read time)
{ stage: 8, delay: 2000 } // Fade out { stage: 8, delay: 2000 }, // Fade out
]; ];
let timeout: any; // Using any instead of NodeJS.Timeout let timeout: any; // Using any instead of NodeJS.Timeout
let currentIndex = 0; let currentIndex = 0;
const runNextStage = () => { const runNextStage = () => {
if (currentIndex < bootSequence.length) { if (currentIndex < bootSequence.length) {
const { stage, delay } = bootSequence[currentIndex]; const { stage, delay } = bootSequence[currentIndex];
// Clean transition - clear ALL previous stages to prevent any background visibility // Clean transition - clear ALL previous stages to prevent any background visibility
document.querySelectorAll('.boot-stage').forEach(el => { document.querySelectorAll(".boot-stage").forEach((el) => {
el.classList.remove('active'); el.classList.remove("active");
}); });
// Reset content visibility // Reset content visibility
document.querySelectorAll('.boot-content').forEach(el => { document.querySelectorAll(".boot-content").forEach((el) => {
el.classList.remove('active'); el.classList.remove("active");
}); });
// Short delay to allow for transition // Short delay to allow for transition
setTimeout(() => { setTimeout(() => {
// First ensure boot content is visible // First ensure boot content is visible
document.querySelectorAll('.boot-content').forEach(el => { document.querySelectorAll(".boot-content").forEach((el) => {
el.classList.add('active'); el.classList.add("active");
}); });
// Then activate the correct stage // Then activate the correct stage
setBootStage(stage); setBootStage(stage);
if (stage === 6) { if (stage === 6) {
setShowLogo(true); setShowLogo(true);
} else if (stage === 7) { } else if (stage === 7) {
setBootComplete(true); setBootComplete(true);
} }
}, 300); }, 300);
currentIndex++; currentIndex++;
timeout = setTimeout(runNextStage, delay); timeout = setTimeout(runNextStage, delay);
} else { } else {
handleAnimationComplete();
}
};
// Start the sequence
timeout = setTimeout(runNextStage, 500);
return () => {
if (timeout) clearTimeout(timeout);
};
}, [skipAnimation]);
const handleAnimationComplete = () => {
setActive(false);
if (onComplete) {
onComplete();
}
};
// Skip button click handler
const handleSkip = () => {
handleAnimationComplete(); handleAnimationComplete();
}
}; };
// Start the sequence if (!active) return null;
timeout = setTimeout(runNextStage, 500);
return (
return () => { <div className="endos-boot-container">
if (timeout) clearTimeout(timeout); {/* Accessibility */}
}; <div className="visually-hidden">
}, [skipAnimation]); Website loading. EndOS boot sequence in progress.
const handleAnimationComplete = () => {
setActive(false);
if (onComplete) {
onComplete();
}
};
// Skip button click handler
const handleSkip = () => {
handleAnimationComplete();
};
if (!active) return null;
return (
<div className="endos-boot-container">
{/* Accessibility */}
<div className="visually-hidden">
Website loading. EndOS boot sequence in progress.
</div>
<div className="boot-scan-line"></div>
<div className="boot-visor-frame">
<div className="visor-left-ear"></div>
<div className="visor-right-ear"></div>
<div className="boot-visor">
<div className="visor-line top"></div>
<div className="visor-line bottom"></div>
{/* Boot Sequence Content */}
<div className={`boot-content`}>
{/* BIOS Check */}
<div className={`boot-stage bios ${bootStage === 2 ? 'active' : ''}`}>
<div className="bios-header">END_OS BIOS v2.5</div>
<div className="boot-text-line">Initializing hardware...</div>
<div className="boot-text-line">CPU: ProtoCore i9 @ 4.7GHz</div>
<div className="boot-text-line">Memory: 16GB NeuralRAM</div>
<div className="boot-text-line">Checking system integrity... OK</div>
<div className="boot-text-line">Starting boot sequence...</div>
</div> </div>
{/* System Scan */} <div className="boot-scan-line"></div>
<div className={`boot-stage scan ${bootStage === 3 ? 'active' : ''}`}>
<div className="scan-header">SYSTEM SCAN</div> <div className="boot-visor-frame">
<div className="scan-progress-container"> <div className="visor-left-ear"></div>
<div className="scan-progress-bar"></div> <div className="visor-right-ear"></div>
</div>
<div className="scan-detail">Checking vital systems...</div> <div className="boot-visor">
<div className="scan-detail">Initializing neural pathways...</div> <div className="visor-line top"></div>
<div className="scan-detail">Activating sensory modules...</div> <div className="visor-line bottom"></div>
<div className="scan-detail">All systems nominal</div>
</div> {/* Boot Sequence Content */}
<div className={`boot-content`}>
{/* Module Loading */} {/* BIOS Check */}
<div className={`boot-stage modules ${bootStage === 4 ? 'active' : ''}`}> <div
<div className="module-header">LOADING CORE MODULES</div> className={`boot-stage bios ${bootStage === 2 ? "active" : ""}`}
<div className="modules-grid"> >
<div className="module-item"> <div className="bios-header">END_OS BIOS v2.5</div>
<div className="module-icon"></div> <div className="boot-text-line">
<div className="module-name">ProtogenCore</div> Initializing hardware...
</div>
<div className="boot-text-line">
CPU: ProtoCore i9 @ 4.7GHz
</div>
<div className="boot-text-line">
Memory: 16GB NeuralRAM
</div>
<div className="boot-text-line">
Checking system integrity... OK
</div>
<div className="boot-text-line">
Starting boot sequence...
</div>
</div>
{/* System Scan */}
<div
className={`boot-stage scan ${bootStage === 3 ? "active" : ""}`}
>
<div className="scan-header">SYSTEM SCAN</div>
<div className="scan-progress-container">
<div className="scan-progress-bar"></div>
</div>
<div className="scan-detail">
Checking vital systems...
</div>
<div className="scan-detail">
Initializing neural pathways...
</div>
<div className="scan-detail">
Activating sensory modules...
</div>
<div className="scan-detail">
All systems nominal
</div>
</div>
{/* Module Loading */}
<div
className={`boot-stage modules ${bootStage === 4 ? "active" : ""}`}
>
<div className="module-header">
LOADING CORE MODULES
</div>
<div className="modules-grid">
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">
ProtogenCore
</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">NeuralNet</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">
VisorDisplay
</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">FoxTraits</div>
</div>
</div>
</div>
{/* Fox Protocol */}
<div
className={`boot-stage fox-protocol ${bootStage === 5 ? "active" : ""}`}
>
<div className="fox-header">
ACTIVATING FOX PROTOCOLS
</div>
<div className="fox-trait">
Fluffy tail module: Online
</div>
<div className="fox-trait">
Fox ears: Calibrated
</div>
<div className="fox-trait">
Cuteness factor: Nonexistent
</div>
<div className="fox-trait">
Mischief subroutines: Loaded
</div>
<div className="fox-trait">
ProtoFox integration: Complete
</div>
</div>
{/* Logo Display */}
<div
className={`boot-stage logo-display ${bootStage === 6 && showLogo ? "active" : ""}`}
>
<div className="endos-logo">
<span className="logo-end">End</span>
<span className="logo-os">OS</span>
</div>
<div className="logo-subtitle">
ProtoFox Operating System
</div>
</div>
{/* System Ready */}
<div
className={`boot-stage system-ready ${bootStage === 7 && bootComplete ? "active" : ""}`}
>
<div className="ready-status">SYSTEM ACTIVATED</div>
<div className="welcome-message">
Welcome back, ProtoFox
</div>
<div className="boot-complete-message">
EndOS v1.0 is fully operational
</div>
</div>
</div>
</div> </div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">NeuralNet</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">VisorDisplay</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">FoxTraits</div>
</div>
</div>
</div> </div>
{/* Fox Protocol */} {/* Skip button - More prominent and always visible */}
<div className={`boot-stage fox-protocol ${bootStage === 5 ? 'active' : ''}`}> <button
<div className="fox-header">ACTIVATING FOX PROTOCOLS</div> className="skip-button"
<div className="fox-trait">Fluffy tail module: Online</div> onClick={handleSkip}
<div className="fox-trait">Fox ears: Calibrated</div> aria-label="Skip boot animation"
<div className="fox-trait">Cuteness factor: Nonexistent</div> >
<div className="fox-trait">Mischief subroutines: Loaded</div> SKIP BOOT SEQUENCE
<div className="fox-trait">ProtoFox integration: Complete</div> </button>
</div>
{/* Logo Display */}
<div className={`boot-stage logo-display ${bootStage === 6 && showLogo ? 'active' : ''}`}>
<div className="endos-logo">
<span className="logo-end">End</span>
<span className="logo-os">OS</span>
</div>
<div className="logo-subtitle">ProtoFox Operating System</div>
</div>
{/* System Ready */}
<div className={`boot-stage system-ready ${bootStage === 7 && bootComplete ? 'active' : ''}`}>
<div className="ready-status">SYSTEM ACTIVATED</div>
<div className="welcome-message">Welcome back, ProtoFox</div>
<div className="boot-complete-message">EndOS v1.0 is fully operational</div>
</div>
</div>
</div> </div>
</div> );
{/* Skip button - More prominent and always visible */}
<button
className="skip-button"
onClick={handleSkip}
aria-label="Skip boot animation"
>
SKIP BOOT SEQUENCE
</button>
</div>
);
}; };
export default EndOSBootAnimation; export default EndOSBootAnimation;

View file

@ -1,5 +1,5 @@
import { Component, ErrorInfo } from 'react'; import { Component, ErrorInfo } from "react";
import { ErrorBoundaryProps, ErrorBoundaryState } from '@/types'; import { ErrorBoundaryProps, ErrorBoundaryState } from "@/types";
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) { constructor(props: ErrorBoundaryProps) {
@ -12,7 +12,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
} }
componentDidCatch(error: Error, errorInfo: ErrorInfo): void { componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error('Error caught by boundary:', error, errorInfo); console.error("Error caught by boundary:", error, errorInfo);
} }
render() { render() {

View file

@ -1,7 +1,7 @@
import type { FoxCardProps } from '@/types'; import type { FoxCardProps } from "@/types";
import '@/styles/FoxCard.css'; import "@/styles/FoxCard.css";
const FoxCard = ({ children, className = '' }: FoxCardProps) => ( const FoxCard = ({ children, className = "" }: FoxCardProps) => (
<div className={`fox-card ${className}`.trim()}> <div className={`fox-card ${className}`.trim()}>
<div className="fox-ear fox-ear-left" /> <div className="fox-ear fox-ear-left" />
<div className="fox-ear fox-ear-right" /> <div className="fox-ear fox-ear-right" />

View file

@ -1,5 +1,5 @@
import { GithubRepo } from '@/types'; import { GithubRepo } from "@/types";
import '@/styles/GithubRepos.css'; import "@/styles/GithubRepos.css";
interface GithubReposProps { interface GithubReposProps {
repos: GithubRepo[]; repos: GithubRepo[];
@ -11,14 +11,21 @@ const GithubRepos: React.FC<GithubReposProps> = ({ repos }) => {
<div className="repos-grid"> <div className="repos-grid">
{repos.map((repo) => ( {repos.map((repo) => (
<div key={repo.id} className="repo-card"> <div key={repo.id} className="repo-card">
<a href={repo.html_url} target="_blank" rel="noopener noreferrer" className="repo-name"> <a
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
className="repo-name"
>
{repo.name} {repo.name}
</a> </a>
<p className="repo-description"> <p className="repo-description">
{repo.description || 'No description provided.'} {repo.description || "No description provided."}
</p> </p>
{repo.language && ( {repo.language && (
<span className="repo-language">{repo.language}</span> <span className="repo-language">
{repo.language}
</span>
)} )}
{repo.languages && repo.languages.length > 0 && ( {repo.languages && repo.languages.length > 0 && (
<div className="repo-languages"> <div className="repo-languages">

View file

@ -1,4 +1,4 @@
import '@/styles/LoadingFox.css'; import "@/styles/LoadingFox.css";
const LoadingFox = () => { const LoadingFox = () => {
return ( return (

View file

@ -1,161 +1,173 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import { Music } from 'lucide-react'; import { Music } from "lucide-react";
interface LastFMImage { interface LastFMImage {
'#text': string; "#text": string;
size: string; size: string;
} }
interface LastFMTrack { interface LastFMTrack {
name: string; name: string;
artist: { artist: {
'#text': string; "#text": string;
}; };
image: LastFMImage[]; image: LastFMImage[];
'@attr'?: { "@attr"?: {
nowplaying: string; nowplaying: string;
}; };
} }
interface LastFMResponse { interface LastFMResponse {
recenttracks: { recenttracks: {
track: LastFMTrack[]; track: LastFMTrack[];
}; };
} }
interface CurrentTrack { interface CurrentTrack {
name: string; name: string;
artist: string; artist: string;
image: string; image: string;
isPlaying: boolean; isPlaying: boolean;
} }
const MusicDisplay = () => { const MusicDisplay = () => {
const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null); const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
useEffect(() => { useEffect(() => {
const fetchCurrentTrack = async () => { const fetchCurrentTrack = async () => {
try { try {
const API_KEY = import.meta.env.VITE_LASTFM_API_KEY; const API_KEY = import.meta.env.VITE_LASTFM_API_KEY;
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME; const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
if (!API_KEY || !USERNAME) { if (!API_KEY || !USERNAME) {
throw new Error('Last.fm API key or username not configured'); throw new Error(
} "Last.fm API key or username not configured",
);
}
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${USERNAME}&api_key=${API_KEY}&format=json&limit=1`; const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${USERNAME}&api_key=${API_KEY}&format=json&limit=1`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data: LastFMResponse = await response.json(); const data: LastFMResponse = await response.json();
if (!data.recenttracks?.track?.length) {
setCurrentTrack(null);
setExpanded(true);
return;
}
const track = data.recenttracks.track[0]; if (!data.recenttracks?.track?.length) {
setCurrentTrack(null);
const largeImage = track.image.find(img => img.size === 'large'); setExpanded(true);
const imageUrl = largeImage ? largeImage['#text'] : return;
track.image[track.image.length - 1] ? track.image[track.image.length - 1]['#text'] : }
'/placeholder-album.jpg';
const isCurrentlyPlaying = track['@attr']?.nowplaying === 'true'; const track = data.recenttracks.track[0];
if (!isCurrentlyPlaying) { const largeImage = track.image.find(
setCurrentTrack(null); (img) => img.size === "large",
setExpanded(true); );
return; const imageUrl = largeImage
} ? largeImage["#text"]
: track.image[track.image.length - 1]
? track.image[track.image.length - 1]["#text"]
: "/placeholder-album.jpg";
setCurrentTrack({ const isCurrentlyPlaying =
name: track.name, track["@attr"]?.nowplaying === "true";
artist: track.artist['#text'],
image: imageUrl,
isPlaying: true
});
setExpanded(false);
setError(null);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An error occurred';
console.error('Last.fm error:', error);
setError(errorMessage);
} finally {
setLoading(false);
}
};
fetchCurrentTrack(); if (!isCurrentlyPlaying) {
const interval = setInterval(fetchCurrentTrack, 30000); setCurrentTrack(null);
return () => clearInterval(interval); setExpanded(true);
}, []); return;
}
setCurrentTrack({
name: track.name,
artist: track.artist["#text"],
image: imageUrl,
isPlaying: true,
});
setExpanded(false);
setError(null);
} catch (error) {
const errorMessage =
error instanceof Error
? error.message
: "An error occurred";
console.error("Last.fm error:", error);
setError(errorMessage);
} finally {
setLoading(false);
}
};
fetchCurrentTrack();
const interval = setInterval(fetchCurrentTrack, 30000);
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<div className="h-[88px] flex items-center justify-center p-4 animate-pulse">
<Music className="w-5 h-5 mr-2" />
<span>Loading music...</span>
</div>
);
}
if (error) {
return (
<div className="h-[88px] flex items-center justify-center p-4 text-red-400">
<span>Unable to load music data: {error}</span>
</div>
);
}
if (!currentTrack) {
return (
<div
className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? "h-[352px]" : "h-[88px]"}`}
>
<iframe
title="Spotify Playlist"
style={{ borderRadius: "12px" }}
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
width="100%"
height="352"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
/>
</div>
);
}
if (loading) {
return ( return (
<div className="h-[88px] flex items-center justify-center p-4 animate-pulse"> <div className="h-[88px] flex items-center gap-4 p-4 bg-gradient-card rounded-lg">
<Music className="w-5 h-5 mr-2" /> <div className="relative w-16 h-16 flex-shrink-0">
<span>Loading music...</span> <img
</div> src={currentTrack.image}
); alt={`${currentTrack.name} album art`}
} className="w-full h-full object-cover rounded-md shadow-lg"
/>
if (error) { <div className="absolute -top-1 -right-1 w-3 h-3">
return ( <span className="absolute w-full h-full bg-accent-neon rounded-full animate-ping"></span>
<div className="h-[88px] flex items-center justify-center p-4 text-red-400"> <span className="absolute w-full h-full bg-accent-neon rounded-full"></span>
<span>Unable to load music data: {error}</span> </div>
</div> </div>
); <div className="flex flex-col min-w-0">
} <span className="font-medium truncate">
{currentTrack.name}
if (!currentTrack) { </span>
return ( <span className="text-sm opacity-75 truncate">
<div className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? 'h-[352px]' : 'h-[88px]'}`}> {currentTrack.artist}
<iframe </span>
title="Spotify Playlist" </div>
style={{ borderRadius: '12px' }}
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
width="100%"
height="352"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
/>
</div>
);
}
return (
<div className="h-[88px] flex items-center gap-4 p-4 bg-gradient-card rounded-lg">
<div className="relative w-16 h-16 flex-shrink-0">
<img
src={currentTrack.image}
alt={`${currentTrack.name} album art`}
className="w-full h-full object-cover rounded-md shadow-lg"
/>
<div className="absolute -top-1 -right-1 w-3 h-3">
<span className="absolute w-full h-full bg-accent-neon rounded-full animate-ping"></span>
<span className="absolute w-full h-full bg-accent-neon rounded-full"></span>
</div> </div>
</div> );
<div className="flex flex-col min-w-0">
<span className="font-medium truncate">
{currentTrack.name}
</span>
<span className="text-sm opacity-75 truncate">
{currentTrack.artist}
</span>
</div>
</div>
);
}; };
export default MusicDisplay; export default MusicDisplay;

View file

@ -1,46 +1,57 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from "react-router-dom";
import { Home, Code, BookOpen, Twitch} from 'lucide-react'; import { Home, Code, BookOpen, Twitch } from "lucide-react";
const Navbar = () => { const Navbar = () => {
const location = useLocation(); const location = useLocation();
const handleRedirect = () => {
window.open("https://vnc.endoftimee.tech", "_blank");
};
return ( return (
<nav className="navbar"> <nav className="navbar">
<div className="nav-content"> <div className="nav-content">
<div className="nav-links"> <div className="nav-links">
<Link <Link
to="/" to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`} className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
> >
<Home size={20} /> <Home size={20} />
<span>About</span> <span>About</span>
</Link> </Link>
<Link <Link
to="/projects" to="/projects"
className={`nav-link ${location.pathname === '/projects' ? 'active' : ''}`} className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
> >
<Code size={20} /> <Code size={20} />
<span>Projects</span> <span>Projects</span>
</Link> </Link>
{/*
<Link <Link
to="/apcsp" to="/apcsp"
className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`} className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`}
> >
<BookOpen size={20} /> <BookOpen size={20} />
<span>APCSP</span> <span>APCSP</span>
</Link> </Link> */}
<Link <button
to="/novnc" onClick={handleRedirect}
className={`nav-link ${location.pathname === '/novnc' ? 'active' : ''}`} className="nav-link"
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "0 15px",
display: "flex",
alignItems: "center",
}}
> >
<Code size={20} /> <Code size={20} />
<span>noVNC</span> <span>NoVNC</span>
</Link> </button>
<a <a
href="https://twitch.tv/EndofTimee" href="https://twitch.tv/EndofTimee"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -49,14 +60,14 @@ const Navbar = () => {
<Twitch size={20} /> <Twitch size={20} />
<span>Stream</span> <span>Stream</span>
</a> </a>
<div className="nav-link"> <div className="nav-link">
<iframe <iframe
src="https://github.com/sponsors/EndofTimee/button" src="https://github.com/sponsors/System-End/button"
title="Sponsor EndofTimee" title="Sponsor End!"
height="32" height="32"
width="114" width="114"
style={{ border: 0, borderRadius: '6px' }} style={{ border: 0, borderRadius: "6px" }}
></iframe> ></iframe>
</div> </div>
</div> </div>
@ -65,4 +76,4 @@ const Navbar = () => {
); );
}; };
export default Navbar; export default Navbar;

View file

@ -1,4 +1,5 @@
import { Navigate, useLocation } from 'react-router-dom'; /** not used */
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { ReactNode } from 'react'; import { ReactNode } from 'react';

View file

@ -1,18 +1,18 @@
const SpotifyEmbed = () => { const SpotifyEmbed = () => {
return ( return (
<div className="w-full aspect-[100/35]"> <div className="w-full aspect-[100/35]">
<iframe <iframe
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator" src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
width="100%" width="100%"
height="100%" height="100%"
style={{ borderRadius: '12px' }} style={{ borderRadius: "12px" }}
frameBorder="0" frameBorder="0"
allowFullScreen allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy" loading="lazy"
/> />
</div> </div>
); );
}; };
export default SpotifyEmbed; export default SpotifyEmbed;

View file

@ -1,109 +1,109 @@
.theme-toggle-container { .theme-toggle-container {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 1rem 0; margin: 1rem 0;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
padding: 0.5rem; padding: 0.5rem;
border-radius: 0.5rem; border-radius: 0.5rem;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
} }
.theme-toggle { .theme-toggle {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
.theme-toggle input { .theme-toggle input {
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
position: absolute; position: absolute;
} }
.toggle-track { .toggle-track {
width: 60px; width: 60px;
height: 30px; height: 30px;
background-color: var(--background-secondary); background-color: var(--background-secondary);
border-radius: 15px; border-radius: 15px;
position: relative; position: relative;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.toggle-indicator { .toggle-indicator {
position: absolute; position: absolute;
width: 26px; width: 26px;
height: 26px; height: 26px;
background-color: var(--text-primary); background-color: var(--text-primary);
border-radius: 50%; border-radius: 50%;
top: 2px; top: 2px;
left: 2px; left: 2px;
transition: all 0.3s ease; transition: all 0.3s ease;
overflow: visible; overflow: visible;
} }
/* Fox ears */ /* Fox ears */
.fox-ear { .fox-ear {
position: absolute; position: absolute;
width: 10px; width: 10px;
height: 10px; height: 10px;
background-color: var(--fox-orange); background-color: var(--fox-orange);
top: -5px; top: -5px;
opacity: 0; opacity: 0;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.fox-ear.left { .fox-ear.left {
left: 3px; left: 3px;
transform: rotate(45deg); transform: rotate(45deg);
border-radius: 0 0 0 5px; border-radius: 0 0 0 5px;
} }
.fox-ear.right { .fox-ear.right {
right: 3px; right: 3px;
transform: rotate(-45deg); transform: rotate(-45deg);
border-radius: 0 0 5px 0; border-radius: 0 0 5px 0;
} }
.fox-ear.active { .fox-ear.active {
opacity: 1; opacity: 1;
} }
/* Visor element */ /* Visor element */
.toggle-visor { .toggle-visor {
position: absolute; position: absolute;
width: 16px; width: 16px;
height: 4px; height: 4px;
background-color: transparent; background-color: transparent;
top: 10px; top: 10px;
left: 5px; left: 5px;
border-radius: 2px; border-radius: 2px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* When toggled to protofox mode */ /* When toggled to protofox mode */
input:checked + .toggle-track { input:checked + .toggle-track {
background-color: var(--background-primary); background-color: var(--background-primary);
} }
input:checked + .toggle-track .toggle-indicator { input:checked + .toggle-track .toggle-indicator {
left: 32px; left: 32px;
background-color: #121212; background-color: #121212;
} }
input:checked + .toggle-track .toggle-visor { input:checked + .toggle-track .toggle-visor {
background-color: #00E5FF; background-color: #00e5ff;
box-shadow: 0 0 5px #00E5FF; box-shadow: 0 0 5px #00e5ff;
} }
.toggle-label { .toggle-label {
margin-left: 10px; margin-left: 10px;
font-size: 14px; font-size: 14px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
input:checked + .toggle-track + .toggle-label { input:checked + .toggle-track + .toggle-label {
color: var(--accent-primary); color: var(--accent-primary);
} }

View file

@ -1,48 +1,52 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import './ThemeToggle.css'; import "./ThemeToggle.css";
interface ThemeToggleProps { interface ThemeToggleProps {
className?: string; className?: string;
} }
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => { const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = "" }) => {
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => { const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
// Check if user had previously selected protofox mode // Check if user had previously selected protofox mode
return localStorage.getItem('theme') === 'protofox'; return localStorage.getItem("theme") === "protofox";
}); });
// Apply theme class to body when toggle changes // Apply theme class to body when toggle changes
useEffect(() => { useEffect(() => {
if (isProtoFoxMode) { if (isProtoFoxMode) {
document.body.classList.add('protofox-theme'); document.body.classList.add("protofox-theme");
localStorage.setItem('theme', 'protofox'); localStorage.setItem("theme", "protofox");
} else { } else {
document.body.classList.remove('protofox-theme'); document.body.classList.remove("protofox-theme");
localStorage.setItem('theme', 'default'); localStorage.setItem("theme", "default");
} }
}, [isProtoFoxMode]); }, [isProtoFoxMode]);
return ( return (
<div className={`theme-toggle-container ${className}`}> <div className={`theme-toggle-container ${className}`}>
<label className="theme-toggle"> <label className="theme-toggle">
<input <input
type="checkbox" type="checkbox"
checked={isProtoFoxMode} checked={isProtoFoxMode}
onChange={() => setIsProtoFoxMode(!isProtoFoxMode)} onChange={() => setIsProtoFoxMode(!isProtoFoxMode)}
/> />
<div className="toggle-track"> <div className="toggle-track">
<div className="toggle-indicator"> <div className="toggle-indicator">
<div className={`fox-ear left ${isProtoFoxMode ? 'active' : ''}`}></div> <div
<div className={`fox-ear right ${isProtoFoxMode ? 'active' : ''}`}></div> className={`fox-ear left ${isProtoFoxMode ? "active" : ""}`}
<div className="toggle-visor"></div> ></div>
</div> <div
className={`fox-ear right ${isProtoFoxMode ? "active" : ""}`}
></div>
<div className="toggle-visor"></div>
</div>
</div>
<span className="toggle-label">
{isProtoFoxMode ? "ProtoFox Mode" : "Standard Mode"}
</span>
</label>
</div> </div>
<span className="toggle-label"> );
{isProtoFoxMode ? 'ProtoFox Mode' : 'Standard Mode'}
</span>
</label>
</div>
);
}; };
export default ThemeToggle; export default ThemeToggle;

View file

@ -1,184 +0,0 @@
import { useState, useEffect } from 'react';
import { Monitor, Power, Lock, Maximize, Minimize, RefreshCw } from 'lucide-react';
import FoxCard from '@/components/FoxCard';
// Ideally, this should come from environment variables
const VNC_HOST = import.meta.env.VITE_VNC_HOST || '68.104.222.58';
const VNC_PORT = import.meta.env.VITE_VNC_PORT || '6080';
const VNC_WS_PORT = import.meta.env.VITE_VNC_WS_PORT || '5901';
const VNCViewer = () => {
const [isConnected, setIsConnected] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Handle fullscreen changes
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const toggleFullscreen = () => {
const iframe = document.getElementById('vnc-iframe');
if (iframe) {
if (!document.fullscreenElement) {
iframe.requestFullscreen().catch(err => {
setError(`Fullscreen error: ${err.message}`);
});
} else {
document.exitFullscreen().catch(err => {
setError(`Exit fullscreen error: ${err.message}`);
});
}
}
};
const handleConnect = () => {
setIsLoading(true);
setError(null);
// Simulate connection process
setTimeout(() => {
try {
setIsConnected(true);
setIsLoading(false);
} catch (error) {
setError('Failed to connect to VNC server');
setIsLoading(false);
}
}, 1000);
};
const handleDisconnect = () => {
setIsConnected(false);
};
const refreshConnection = () => {
if (isConnected) {
setIsConnected(false);
setTimeout(() => setIsConnected(true), 500);
}
};
const connectionUrl = `http://${VNC_HOST}:${VNC_PORT}/vnc.html?host=${VNC_HOST}&port=${VNC_WS_PORT}&autoconnect=true&resize=scale`;
return (
<div className="page-container">
<FoxCard className="header-card">
<h1 className="text-glow">Remote Access</h1>
<p className="text-gradient">Raspberry Pi VNC Connection</p>
</FoxCard>
<FoxCard className="w-full max-w-6xl mx-auto overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-accent-primary/20">
<div className="flex items-center gap-3">
<Monitor className="text-accent-primary" size={24} />
<h2 className="text-xl font-semibold text-text-primary">Raspberry Pi Remote Access</h2>
</div>
<div className="flex items-center gap-2">
{isConnected && (
<>
<button
onClick={refreshConnection}
className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
title="Refresh Connection"
>
<RefreshCw size={20} className="text-accent-primary" />
</button>
<button
onClick={toggleFullscreen}
className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
title={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
>
{isFullscreen ? (
<Minimize size={20} className="text-accent-primary" />
) : (
<Maximize size={20} className="text-accent-primary" />
)}
</button>
</>
)}
<button
onClick={isConnected ? handleDisconnect : handleConnect}
className={`p-2 rounded-lg transition-colors ${
isConnected
? "bg-red-500/20 hover:bg-red-500/30 text-red-400"
: "hover:bg-accent-primary/20 text-accent-primary"
}`}
title={isConnected ? "Disconnect" : "Connect"}
disabled={isLoading}
>
<Power
size={20}
className={isConnected ? "text-red-400" : "text-accent-primary"}
/>
</button>
</div>
</div>
<div className="aspect-video w-full bg-black/50 relative">
{isConnected ? (
<iframe
id="vnc-iframe"
src={connectionUrl}
className="w-full h-full border-0"
allow="fullscreen"
/>
) : isLoading ? (
<div className="absolute inset-0 flex flex-col items-center justify-center text-text-primary/60">
<div className="w-12 h-12 border-4 border-accent-primary/20 border-t-accent-primary rounded-full animate-spin mb-4" />
<p>Connecting to VNC server...</p>
</div>
) : (
<div className="absolute inset-0 flex flex-col items-center justify-center text-text-primary/60">
<Lock size={48} className="mb-4 text-accent-primary/40" />
<p>Click the power button to connect</p>
</div>
)}
{error && (
<div className="absolute bottom-0 left-0 right-0 bg-red-500/80 text-white p-2 text-center text-sm">
{error}
</div>
)}
</div>
<div className="p-4 border-t border-accent-primary/20 flex flex-col sm:flex-row sm:justify-between items-center gap-2 text-sm text-text-primary/60">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
<span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
</div>
<div className="flex items-center flex-wrap justify-center gap-x-4 gap-y-2">
<span>Server: {VNC_HOST}</span>
<span>Press ESC to exit fullscreen</span>
</div>
</div>
</FoxCard>
<div className="mt-6 max-w-6xl mx-auto">
<FoxCard>
<h3 className="text-lg font-semibold mb-2">Connection Instructions</h3>
<ol className="list-decimal pl-5 space-y-2">
<li>Click the power button in the top right to connect</li>
<li>Once connected, you can interact with the remote desktop</li>
<li>Use the maximize button to enter fullscreen mode</li>
<li>If the connection is unresponsive, try clicking the refresh button</li>
</ol>
<div className="mt-4 p-3 bg-background-secondary/30 rounded-lg text-sm">
<p className="text-yellow-300">Note: This VNC connection is not encrypted. Do not transmit sensitive information when using this interface.</p>
</div>
</FoxCard>
</div>
</div>
);
};
export default VNCViewer;

View file

@ -1,17 +1,17 @@
import { type CardProps } from '@/types'; import { type CardProps } from "@/types";
export const Card: React.FC<CardProps> = ({ className, children }) => ( export const Card: React.FC<CardProps> = ({ className, children }) => (
<div className={`card ${className || ''}`}>{children}</div> <div className={`card ${className || ""}`}>{children}</div>
); );
export const CardHeader: React.FC<CardProps> = ({ className, children }) => ( export const CardHeader: React.FC<CardProps> = ({ className, children }) => (
<div className={`card-header ${className || ''}`}>{children}</div> <div className={`card-header ${className || ""}`}>{children}</div>
); );
export const CardTitle: React.FC<CardProps> = ({ className, children }) => ( export const CardTitle: React.FC<CardProps> = ({ className, children }) => (
<h2 className={`card-title ${className || ''}`}>{children}</h2> <h2 className={`card-title ${className || ""}`}>{children}</h2>
); );
export const CardContent: React.FC<CardProps> = ({ className, children }) => ( export const CardContent: React.FC<CardProps> = ({ className, children }) => (
<div className={`card-content ${className || ''}`}>{children}</div> <div className={`card-content ${className || ""}`}>{children}</div>
); );

View file

@ -1,163 +1,177 @@
import { createContext, useContext, useState, ReactNode, useEffect } from 'react'; import {
createContext,
useContext,
useState,
ReactNode,
useEffect,
} from "react";
interface SystemMember { interface SystemMember {
id: string; id: string;
name: string; name: string;
role: string; role: string;
color?: string; color?: string;
} }
interface SystemState { interface SystemState {
safetyLevel: 'safe' | 'unsafe' | 'sorta-safe' | 'unknown'; safetyLevel: "safe" | "unsafe" | "sorta-safe" | "unknown";
mentalState: 'ok' | 'bad' | 'very-bad' | 'panic' | 'spiraling' | 'unstable' | 'delusional'; mentalState:
frontingStatus: 'single' | 'co-fronting' | 'switching' | 'unknown'; | "ok"
currentFronters: SystemMember[]; | "bad"
| "very-bad"
| "panic"
| "spiraling"
| "unstable"
| "delusional";
frontingStatus: "single" | "co-fronting" | "switching" | "unknown";
currentFronters: SystemMember[];
} }
interface AuthContextType { interface AuthContextType {
isAuthenticated: boolean; isAuthenticated: boolean;
username: string | null; username: string | null;
systemState: SystemState | null; systemState: SystemState | null;
login: (username: string, password: string) => Promise<boolean>; login: (username: string, password: string) => Promise<boolean>;
logout: () => void; logout: () => void;
updateSystemState: (newState: Partial<SystemState>) => void; updateSystemState: (newState: Partial<SystemState>) => void;
} }
// System members data // System members data
const systemMembers: SystemMember[] = [ const systemMembers: SystemMember[] = [
{ id: '1', name: 'Aurora', role: 'Host', color: '#9d4edd' }, { id: "1", name: "Aurora", role: "Host", color: "#9d4edd" },
{ id: '2', name: 'Alex', role: 'Younger', color: '#4ea8de' }, { id: "2", name: "Alex", role: "Younger", color: "#4ea8de" },
{ id: '3', name: 'Psy', role: 'Protector', color: '#5e548e' }, { id: "3", name: "Psy", role: "Protector", color: "#5e548e" },
{ id: '4', name: 'Xander', role: 'Caretaker', color: '#219ebc' }, { id: "4", name: "Xander", role: "Caretaker", color: "#219ebc" },
{ id: '5', name: 'Unknown', role: 'Fragment', color: '#6c757d' }, { id: "5", name: "Unknown", role: "Fragment", color: "#6c757d" },
{ id: '6', name: 'The thing', role: 'Persecutor', color: '#e63946' }, { id: "6", name: "The thing", role: "Persecutor", color: "#e63946" },
{ id: '7', name: 'Unknown 2', role: 'Fragment', color: '#6c757d' }, { id: "7", name: "Unknown 2", role: "Fragment", color: "#6c757d" },
]; ];
// Creating the context with a default value of null // Creating the context with a default value of null
const AuthContext = createContext<AuthContextType | null>(null); const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: ReactNode }) => { export const AuthProvider = ({ children }: { children: ReactNode }) => {
// Initialize authentication state from localStorage if available // Initialize authentication state from localStorage if available
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => { const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
const stored = localStorage.getItem('isAuthenticated'); const stored = localStorage.getItem("isAuthenticated");
return stored === 'true'; return stored === "true";
}); });
const [username, setUsername] = useState<string | null>(() => { const [username, setUsername] = useState<string | null>(() => {
return localStorage.getItem('username'); return localStorage.getItem("username");
}); });
// Initialize system state from localStorage or set defaults // Initialize system state from localStorage or set defaults
const [systemState, setSystemState] = useState<SystemState | null>(() => { const [systemState, setSystemState] = useState<SystemState | null>(() => {
const stored = localStorage.getItem('systemState'); const stored = localStorage.getItem("systemState");
if (stored && isAuthenticated) { if (stored && isAuthenticated) {
return JSON.parse(stored); return JSON.parse(stored);
} }
return isAuthenticated ? { return isAuthenticated
safetyLevel: 'safe', ? {
mentalState: 'ok', safetyLevel: "safe",
frontingStatus: 'single', mentalState: "ok",
currentFronters: [systemMembers[0]] // Default to Aurora as fronter frontingStatus: "single",
} : null; currentFronters: [systemMembers[0]], // Default to Aurora as fronter
}); }
: null;
});
// Update localStorage when auth state changes // Update localStorage when auth state changes
useEffect(() => { useEffect(() => {
localStorage.setItem('isAuthenticated', isAuthenticated.toString()); localStorage.setItem("isAuthenticated", isAuthenticated.toString());
if (username) { if (username) {
localStorage.setItem('username', username); localStorage.setItem("username", username);
} else { } else {
localStorage.removeItem('username'); localStorage.removeItem("username");
} }
// If logged out, clear system state // If logged out, clear system state
if (!isAuthenticated) { if (!isAuthenticated) {
localStorage.removeItem('systemState'); localStorage.removeItem("systemState");
setSystemState(null); setSystemState(null);
} else if (systemState) { } else if (systemState) {
localStorage.setItem('systemState', JSON.stringify(systemState)); localStorage.setItem("systemState", JSON.stringify(systemState));
} }
}, [isAuthenticated, username, systemState]); }, [isAuthenticated, username, systemState]);
const login = async (username: string, password: string) => { const login = async (username: string, password: string) => {
// For security, add a slight delay to prevent rapid brute force attempts // For security, add a slight delay to prevent rapid brute force attempts
await new Promise(resolve => setTimeout(resolve, 800)); await new Promise((resolve) => setTimeout(resolve, 800));
// We use credential verification with multiple allowed passwords for different contexts // We use credential verification with multiple allowed passwords for different contexts
const validCredentials = [ const validCredentials = [{ user: "system", pass: "." }];
{ user: 'system', pass: '.' },
]; const isValid = validCredentials.some(
(cred) =>
const isValid = validCredentials.some( cred.user === username.toLowerCase() && cred.pass === password,
cred => cred.user === username.toLowerCase() && cred.pass === password );
if (isValid) {
setIsAuthenticated(true);
setUsername(username);
// Initialize system state on login
const initialState: SystemState = {
safetyLevel: "safe",
mentalState: "ok",
frontingStatus: "single",
currentFronters: [systemMembers[0]],
};
setSystemState(initialState);
localStorage.setItem("systemState", JSON.stringify(initialState));
return true;
}
return false;
};
const logout = () => {
// Add a short delay for better UX
setTimeout(() => {
setIsAuthenticated(false);
setUsername(null);
setSystemState(null);
// Clear sensitive data from localStorage
localStorage.removeItem("systemState");
}, 300);
};
const updateSystemState = (newState: Partial<SystemState>) => {
if (!systemState) return;
const updatedState = { ...systemState, ...newState };
setSystemState(updatedState);
localStorage.setItem("systemState", JSON.stringify(updatedState));
};
// Construct the context value
const contextValue: AuthContextType = {
isAuthenticated,
username,
systemState,
login,
logout,
updateSystemState,
};
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
); );
if (isValid) {
setIsAuthenticated(true);
setUsername(username);
// Initialize system state on login
const initialState: SystemState = {
safetyLevel: 'safe',
mentalState: 'ok',
frontingStatus: 'single',
currentFronters: [systemMembers[0]]
};
setSystemState(initialState);
localStorage.setItem('systemState', JSON.stringify(initialState));
return true;
}
return false;
};
const logout = () => {
// Add a short delay for better UX
setTimeout(() => {
setIsAuthenticated(false);
setUsername(null);
setSystemState(null);
// Clear sensitive data from localStorage
localStorage.removeItem('systemState');
}, 300);
};
const updateSystemState = (newState: Partial<SystemState>) => {
if (!systemState) return;
const updatedState = { ...systemState, ...newState };
setSystemState(updatedState);
localStorage.setItem('systemState', JSON.stringify(updatedState));
};
// Construct the context value
const contextValue: AuthContextType = {
isAuthenticated,
username,
systemState,
login,
logout,
updateSystemState
};
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
}; };
// Custom hook for easier context consumption // Custom hook for easier context consumption
export const useAuth = () => { export const useAuth = () => {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (!context) { if (!context) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error("useAuth must be used within an AuthProvider");
} }
return context; return context;
}; };
// Export system members data for use in other components // Export system members data for use in other components

10
src/env.d.ts vendored
View file

@ -1,12 +1,12 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_SPOTIFY_CLIENT_ID: string readonly VITE_SPOTIFY_CLIENT_ID: string;
readonly VITE_SPOTIFY_CLIENT_SECRET: string readonly VITE_SPOTIFY_CLIENT_SECRET: string;
readonly VITE_SPOTIFY_REDIRECT_URI: string readonly VITE_SPOTIFY_REDIRECT_URI: string;
readonly VITE_WORKER_URL: string readonly VITE_WORKER_URL: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv;
} }

View file

@ -1,100 +1,107 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from "react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
import { useGameLoop } from '../hooks/useGameLoop'; import { useGameLoop } from "../hooks/useGameLoop";
import { useGameControls } from '../hooks/useGameControls'; import { useGameControls } from "../hooks/useGameControls";
import { Player } from './Player'; import { Player } from "./Player";
import { GameHUD } from './GameHUD'; import { GameHUD } from "./GameHUD";
import { GameOverlay } from './GameOverlay'; import { GameOverlay } from "./GameOverlay";
const FoxGame: React.FC = () => { const FoxGame: React.FC = () => {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const gameStore = useGameStore(); const gameStore = useGameStore();
// Initialize game systems
useGameLoop();
useGameControls();
// Konami code activation // Initialize game systems
useEffect(() => { useGameLoop();
const konamiCode = [ useGameControls();
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'b', 'a'
];
let index = 0;
const handleKeydown = (event: KeyboardEvent) => { // Konami code activation
if (event.key === konamiCode[index]) { useEffect(() => {
index++; const konamiCode = [
if (index === konamiCode.length) { "ArrowUp",
setIsActive(true); "ArrowUp",
gameStore.startNewGame(); "ArrowDown",
} "ArrowDown",
} else { "ArrowLeft",
index = 0; "ArrowRight",
} "ArrowLeft",
}; "ArrowRight",
"b",
"a",
];
let index = 0;
window.addEventListener('keydown', handleKeydown); const handleKeydown = (event: KeyboardEvent) => {
return () => window.removeEventListener('keydown', handleKeydown); if (event.key === konamiCode[index]) {
}, []); index++;
if (index === konamiCode.length) {
setIsActive(true);
gameStore.startNewGame();
}
} else {
index = 0;
}
};
if (!isActive) return null; window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}, []);
return ( if (!isActive) return null;
<div className="fixed inset-0 bg-gradient-to-b from-background-primary to-background-secondary z-50">
<div className="relative w-full h-full overflow-hidden game-viewport"> return (
{/* Game world */} <div className="fixed inset-0 bg-gradient-to-b from-background-primary to-background-secondary z-50">
<div className="absolute inset-0"> <div className="relative w-full h-full overflow-hidden game-viewport">
<Player /> {/* Game world */}
<div className="absolute inset-0">
{/* Render collectibles */} <Player />
{gameStore.collectibles.map(collectible => (
<div {/* Render collectibles */}
key={collectible.id} {gameStore.collectibles.map((collectible) => (
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${ <div
collectible.type === 'GEM' ? 'bg-purple-500' : 'bg-yellow-400' key={collectible.id}
}`} className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
style={{ collectible.type === "GEM"
left: `${collectible.position.x}%`, ? "bg-purple-500"
top: `${collectible.position.y}%` : "bg-yellow-400"
}} }`}
/> style={{
))} left: `${collectible.position.x}%`,
top: `${collectible.position.y}%`,
{/* Render enemies */} }}
{gameStore.enemies.map(enemy => ( />
<div ))}
key={enemy.id}
className="absolute w-6 h-6 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2" {/* Render enemies */}
style={{ {gameStore.enemies.map((enemy) => (
left: `${enemy.position.x}%`, <div
top: `${enemy.position.y}%` key={enemy.id}
}} className="absolute w-6 h-6 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2"
/> style={{
))} left: `${enemy.position.x}%`,
top: `${enemy.position.y}%`,
{/* Render power-ups */} }}
{gameStore.powerUps.map(powerUp => ( />
<div ))}
key={powerUp.id}
className="absolute w-8 h-8 bg-accent-neon rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-float" {/* Render power-ups */}
style={{ {gameStore.powerUps.map((powerUp) => (
left: `${powerUp.position.x}%`, <div
top: `${powerUp.position.y}%` key={powerUp.id}
}} className="absolute w-8 h-8 bg-accent-neon rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-float"
/> style={{
))} left: `${powerUp.position.x}%`,
top: `${powerUp.position.y}%`,
}}
/>
))}
</div>
{/* HUD and Overlay */}
<GameHUD />
<GameOverlay />
</div>
</div> </div>
);
{/* HUD and Overlay */}
<GameHUD />
<GameOverlay />
</div>
</div>
);
}; };
export default FoxGame; export default FoxGame;

View file

@ -1,72 +1,83 @@
import React from 'react'; import React from "react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
import { Heart, Star, Timer, Trophy } from 'lucide-react'; import { Heart, Star, Timer, Trophy } from "lucide-react";
export const GameHUD: React.FC = () => { export const GameHUD: React.FC = () => {
const { player, score, level, timePlayed } = useGameStore(); const { player, score, level, timePlayed } = useGameStore();
const formatTime = (ms: number) => { const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000); const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`; return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
}; };
return ( return (
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none"> <div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
{/* Left section - Health and PowerUps */} {/* Left section - Health and PowerUps */}
<div className="space-y-4"> <div className="space-y-4">
{/* Health bar */} {/* Health bar */}
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"> <div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
<Heart <Heart
className={`w-6 h-6 ${ className={`w-6 h-6 ${
player.health > 20 ? 'text-red-500' : 'text-red-500 animate-pulse' player.health > 20
}`} ? "text-red-500"
/> : "text-red-500 animate-pulse"
<div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden"> }`}
<div />
className="h-full bg-red-500 transition-all duration-300" <div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden">
style={{ width: `${player.health}%` }} <div
/> className="h-full bg-red-500 transition-all duration-300"
</div> style={{ width: `${player.health}%` }}
</div> />
</div>
</div>
{/* Active power-ups */} {/* Active power-ups */}
<div className="flex gap-2"> <div className="flex gap-2">
{player.powerUps.map((powerUp) => ( {player.powerUps.map((powerUp) => (
<div <div
key={powerUp.id} key={powerUp.id}
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm" className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
> >
<div className="w-8 h-8 relative"> <div className="w-8 h-8 relative">
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" /> <div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
</div> </div>
</div>
))}
</div>
</div> </div>
))}
</div>
</div>
{/* Center - Score and Level */} {/* Center - Score and Level */}
<div className="absolute left-1/2 top-0 -translate-x-1/2 text-center space-y-2"> <div className="absolute left-1/2 top-0 -translate-x-1/2 text-center space-y-2">
<div className="bg-background-primary/50 px-4 py-2 rounded-lg backdrop-blur-sm"> <div className="bg-background-primary/50 px-4 py-2 rounded-lg backdrop-blur-sm">
<div className="text-2xl font-bold text-accent-neon">Level {level}</div> <div className="text-2xl font-bold text-accent-neon">
<div className="flex items-center justify-center gap-2"> Level {level}
<Star className="w-5 h-5 text-yellow-400" /> </div>
<span className="text-xl">{score.toLocaleString()}</span> <div className="flex items-center justify-center gap-2">
</div> <Star className="w-5 h-5 text-yellow-400" />
</div> <span className="text-xl">
</div> {score.toLocaleString()}
</span>
</div>
</div>
</div>
{/* Right section - Time and High Score */} {/* Right section - Time and High Score */}
<div className="space-y-4 text-right"> <div className="space-y-4 text-right">
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2"> <div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
<Timer className="w-5 h-5 text-accent-primary" /> <Timer className="w-5 h-5 text-accent-primary" />
<span>{formatTime(timePlayed)}</span> <span>{formatTime(timePlayed)}</span>
</div>
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
<Trophy className="w-5 h-5 text-yellow-400" />
<span>
Best:{" "}
{Math.max(
...useGameStore.getState().highScores,
).toLocaleString()}
</span>
</div>
</div>
</div> </div>
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2"> );
<Trophy className="w-5 h-5 text-yellow-400" /> };
<span>Best: {Math.max(...useGameStore.getState().highScores).toLocaleString()}</span>
</div>
</div>
</div>
);
};

View file

@ -1,78 +1,89 @@
// src/games/fox-adventure/components/GameOverlay.tsx // src/games/fox-adventure/components/GameOverlay.tsx
import { Play, RotateCcw } from 'lucide-react'; import { Play, RotateCcw } from "lucide-react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
export const GameOverlay: React.FC = () => { export const GameOverlay: React.FC = () => {
const { gameStatus, score, startNewGame, resumeGame } = useGameStore(); const { gameStatus, score, startNewGame, resumeGame } = useGameStore();
if (gameStatus === 'PLAYING') return null; if (gameStatus === "PLAYING") return null;
return ( return (
<div className="absolute inset-0 bg-background-primary/80 backdrop-blur-md flex items-center justify-center"> <div className="absolute inset-0 bg-background-primary/80 backdrop-blur-md flex items-center justify-center">
<div className="max-w-md w-full p-8 bg-gradient-card rounded-xl border border-accent-primary/20"> <div className="max-w-md w-full p-8 bg-gradient-card rounded-xl border border-accent-primary/20">
{gameStatus === 'MENU' && ( {gameStatus === "MENU" && (
<div className="text-center space-y-6"> <div className="text-center space-y-6">
<h1 className="text-4xl font-bold text-glow">Fox Adventure</h1> <h1 className="text-4xl font-bold text-glow">
<p className="text-lg text-text-primary/80">Help the fox collect treasures while avoiding enemies!</p> Fox Adventure
<div className="space-y-4"> </h1>
<h2 className="text-xl font-semibold">Controls:</h2> <p className="text-lg text-text-primary/80">
<ul className="text-left space-y-2"> Help the fox collect treasures while avoiding
<li>Move: Arrow keys or WASD</li> enemies!
<li>Pause: ESC</li> </p>
<li>Collect items to score points</li> <div className="space-y-4">
<li>Find power-ups to gain advantages</li> <h2 className="text-xl font-semibold">Controls:</h2>
<li>Avoid enemies or find shields</li> <ul className="text-left space-y-2">
</ul> <li>Move: Arrow keys or WASD</li>
<li>Pause: ESC</li>
<li>Collect items to score points</li>
<li>Find power-ups to gain advantages</li>
<li>Avoid enemies or find shields</li>
</ul>
</div>
<button
onClick={startNewGame}
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
>
<Play className="w-5 h-5 group-hover:animate-pulse" />
Start Game
</button>
</div>
)}
{gameStatus === "PAUSED" && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-glow">
Game Paused
</h2>
<div className="flex justify-center gap-4">
<button
onClick={resumeGame}
className="px-6 py-3 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center gap-2 group"
>
<Play className="w-5 h-5 group-hover:animate-pulse" />
Resume
</button>
<button
onClick={startNewGame}
className="px-6 py-3 bg-background-secondary hover:bg-background-secondary/80 transition-all rounded-lg flex items-center gap-2"
>
<RotateCcw className="w-5 h-5" />
Restart
</button>
</div>
</div>
)}
{gameStatus === "GAME_OVER" && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-red-500">
Game Over
</h2>
<div className="space-y-2">
<p className="text-xl">Final Score:</p>
<p className="text-4xl font-bold text-accent-neon">
{score.toLocaleString()}
</p>
</div>
<button
onClick={startNewGame}
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
>
<RotateCcw className="w-5 h-5 group-hover:animate-pulse" />
Play Again
</button>
</div>
)}
</div> </div>
<button </div>
onClick={startNewGame} );
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group" };
>
<Play className="w-5 h-5 group-hover:animate-pulse" />
Start Game
</button>
</div>
)}
{gameStatus === 'PAUSED' && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-glow">Game Paused</h2>
<div className="flex justify-center gap-4">
<button
onClick={resumeGame}
className="px-6 py-3 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center gap-2 group"
>
<Play className="w-5 h-5 group-hover:animate-pulse" />
Resume
</button>
<button
onClick={startNewGame}
className="px-6 py-3 bg-background-secondary hover:bg-background-secondary/80 transition-all rounded-lg flex items-center gap-2"
>
<RotateCcw className="w-5 h-5" />
Restart
</button>
</div>
</div>
)}
{gameStatus === 'GAME_OVER' && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-red-500">Game Over</h2>
<div className="space-y-2">
<p className="text-xl">Final Score:</p>
<p className="text-4xl font-bold text-accent-neon">{score.toLocaleString()}</p>
</div>
<button
onClick={startNewGame}
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
>
<RotateCcw className="w-5 h-5 group-hover:animate-pulse" />
Play Again
</button>
</div>
)}
</div>
</div>
);
};

View file

@ -1,64 +1,64 @@
import React from 'react'; import React from "react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
export const Player: React.FC = () => { export const Player: React.FC = () => {
const player = useGameStore(state => state.player); const player = useGameStore((state) => state.player);
return ( return (
<div <div
className={`absolute transition-all duration-100 ${ className={`absolute transition-all duration-100 ${
player.isInvincible ? 'animate-pulse' : '' player.isInvincible ? "animate-pulse" : ""
}`} }`}
style={{ style={{
left: `${player.position.x}%`, left: `${player.position.x}%`,
top: `${player.position.y}%`, top: `${player.position.y}%`,
transform: 'translate(-50%, -50%)' transform: "translate(-50%, -50%)",
}} }}
> >
{/* Fox body */} {/* Fox body */}
<div className="relative w-16 h-16"> <div className="relative w-16 h-16">
{/* Main body */} {/* Main body */}
<div className="absolute inset-0 bg-fox-orange rounded-full"> <div className="absolute inset-0 bg-fox-orange rounded-full">
{/* Face */} {/* Face */}
<div className="absolute inset-0"> <div className="absolute inset-0">
{/* Eyes */} {/* Eyes */}
<div className="absolute top-1/3 left-1/4 w-2 h-2 bg-dark-accent rounded-full" /> <div className="absolute top-1/3 left-1/4 w-2 h-2 bg-dark-accent rounded-full" />
<div className="absolute top-1/3 right-1/4 w-2 h-2 bg-dark-accent rounded-full" /> <div className="absolute top-1/3 right-1/4 w-2 h-2 bg-dark-accent rounded-full" />
{/* Nose */}
<div className="absolute top-1/2 left-1/2 w-2 h-2 bg-dark-accent rounded-full transform -translate-x-1/2" />
</div>
{/* Ears */} {/* Nose */}
<div className="absolute -top-4 -left-2 w-4 h-4 bg-fox-orange transform rotate-45" /> <div className="absolute top-1/2 left-1/2 w-2 h-2 bg-dark-accent rounded-full transform -translate-x-1/2" />
<div className="absolute -top-4 -right-2 w-4 h-4 bg-fox-orange transform -rotate-45" /> </div>
{/* Tail */} {/* Ears */}
<div className="absolute -bottom-4 left-1/2 w-3 h-6 bg-fox-orange rounded-full transform -translate-x-1/2 origin-top animate-wag" /> <div className="absolute -top-4 -left-2 w-4 h-4 bg-fox-orange transform rotate-45" />
<div className="absolute -top-4 -right-2 w-4 h-4 bg-fox-orange transform -rotate-45" />
{/* Tail */}
<div className="absolute -bottom-4 left-1/2 w-3 h-6 bg-fox-orange rounded-full transform -translate-x-1/2 origin-top animate-wag" />
</div>
{/* Power-up effects */}
{player.isInvincible && (
<div className="absolute inset-0 rounded-full border-4 border-accent-neon animate-pulse" />
)}
{/* Key indicator */}
{player.hasKey && (
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
<div className="w-4 h-4 bg-accent-primary rounded-full animate-float" />
</div>
)}
{/* Health indicator */}
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-12">
<div className="w-full h-1 bg-background-secondary rounded-full overflow-hidden">
<div
className="h-full bg-red-500 transition-all duration-300"
style={{ width: `${player.health}%` }}
/>
</div>
</div>
</div>
</div> </div>
);
{/* Power-up effects */} };
{player.isInvincible && (
<div className="absolute inset-0 rounded-full border-4 border-accent-neon animate-pulse" />
)}
{/* Key indicator */}
{player.hasKey && (
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
<div className="w-4 h-4 bg-accent-primary rounded-full animate-float" />
</div>
)}
{/* Health indicator */}
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-12">
<div className="w-full h-1 bg-background-secondary rounded-full overflow-hidden">
<div
className="h-full bg-red-500 transition-all duration-300"
style={{ width: `${player.health}%` }}
/>
</div>
</div>
</div>
</div>
);
};

View file

@ -1,62 +1,64 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
export const useGameControls = () => { export const useGameControls = () => {
const gameStore = useGameStore(); const gameStore = useGameStore();
useEffect(() => { useEffect(() => {
const keys = new Set<string>(); const keys = new Set<string>();
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
keys.add(e.key); keys.add(e.key);
if (e.key === 'Escape') {
if (gameStore.gameStatus === 'PLAYING') {
gameStore.pauseGame();
} else if (gameStore.gameStatus === 'PAUSED') {
gameStore.resumeGame();
}
}
};
const handleKeyUp = (e: KeyboardEvent) => { if (e.key === "Escape") {
keys.delete(e.key); if (gameStore.gameStatus === "PLAYING") {
}; gameStore.pauseGame();
} else if (gameStore.gameStatus === "PAUSED") {
gameStore.resumeGame();
}
}
};
const updatePlayerMovement = () => { const handleKeyUp = (e: KeyboardEvent) => {
if (gameStore.gameStatus !== 'PLAYING') return; keys.delete(e.key);
};
const direction = { x: 0, y: 0 }; const updatePlayerMovement = () => {
if (gameStore.gameStatus !== "PLAYING") return;
if (keys.has('ArrowUp') || keys.has('w')) direction.y -= 1; const direction = { x: 0, y: 0 };
if (keys.has('ArrowDown') || keys.has('s')) direction.y += 1;
if (keys.has('ArrowLeft') || keys.has('a')) direction.x -= 1;
if (keys.has('ArrowRight') || keys.has('d')) direction.x += 1;
if (direction.x !== 0 || direction.y !== 0) { if (keys.has("ArrowUp") || keys.has("w")) direction.y -= 1;
// Normalize diagonal movement if (keys.has("ArrowDown") || keys.has("s")) direction.y += 1;
const magnitude = Math.sqrt(direction.x * direction.x + direction.y * direction.y); if (keys.has("ArrowLeft") || keys.has("a")) direction.x -= 1;
direction.x /= magnitude; if (keys.has("ArrowRight") || keys.has("d")) direction.x += 1;
direction.y /= magnitude;
gameStore.movePlayer(direction);
}
};
let animationFrameId: number; if (direction.x !== 0 || direction.y !== 0) {
const gameLoop = () => { // Normalize diagonal movement
updatePlayerMovement(); const magnitude = Math.sqrt(
animationFrameId = requestAnimationFrame(gameLoop); direction.x * direction.x + direction.y * direction.y,
}; );
animationFrameId = requestAnimationFrame(gameLoop); direction.x /= magnitude;
direction.y /= magnitude;
window.addEventListener('keydown', handleKeyDown); gameStore.movePlayer(direction);
window.addEventListener('keyup', handleKeyUp); }
};
return () => { let animationFrameId: number;
window.removeEventListener('keydown', handleKeyDown); const gameLoop = () => {
window.removeEventListener('keyup', handleKeyUp); updatePlayerMovement();
cancelAnimationFrame(animationFrameId); animationFrameId = requestAnimationFrame(gameLoop);
}; };
}, []); animationFrameId = requestAnimationFrame(gameLoop);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
cancelAnimationFrame(animationFrameId);
};
}, []);
};

View file

@ -1,79 +1,78 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import useGameStore from '../state/gameStore'; import useGameStore from "../state/gameStore";
export const useGameLoop = () => { export const useGameLoop = () => {
const frameRef = useRef<number>(); const frameRef = useRef<number>();
const lastUpdateRef = useRef<number>(0); const lastUpdateRef = useRef<number>(0);
const lastSpawnRef = useRef<number>(0); const lastSpawnRef = useRef<number>(0);
const gameStore = useGameStore(); const gameStore = useGameStore();
useEffect(() => { useEffect(() => {
const spawnCollectible = () => { const spawnCollectible = () => {
const now = Date.now(); const now = Date.now();
if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds
lastSpawnRef.current = now;
const collectible: any = {
id: `collectible-${now}`,
type: Math.random() > 0.8 ? 'GEM' : 'STAR',
value: Math.random() > 0.8 ? 10 : 5,
position: {
x: Math.random() * 90 + 5,
y: Math.random() * 90 + 5
}
};
gameStore.collectibles.push(collectible);
};
const gameLoop = (timestamp: number) => { lastSpawnRef.current = now;
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp; const collectible: any = {
const deltaTime = timestamp - lastUpdateRef.current; id: `collectible-${now}`,
type: Math.random() > 0.8 ? "GEM" : "STAR",
value: Math.random() > 0.8 ? 10 : 5,
position: {
x: Math.random() * 90 + 5,
y: Math.random() * 90 + 5,
},
};
if (gameStore.gameStatus === 'PLAYING') { gameStore.collectibles.push(collectible);
// Update entities };
gameStore.updateEnemies(deltaTime);
// Spawn collectibles
spawnCollectible();
// Check collisions const gameLoop = (timestamp: number) => {
const { player, enemies, collectibles } = gameStore; if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
const deltaTime = timestamp - lastUpdateRef.current;
// Enemy collisions
enemies.forEach(enemy => {
const dx = player.position.x - enemy.position.x;
const dy = player.position.y - enemy.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5 && !player.isInvincible) {
gameStore.takeDamage(20);
}
});
// Collectible collisions if (gameStore.gameStatus === "PLAYING") {
collectibles.forEach(collectible => { // Update entities
const dx = player.position.x - collectible.position.x; gameStore.updateEnemies(deltaTime);
const dy = player.position.y - collectible.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5) {
gameStore.collectItem(collectible.id);
}
});
}
lastUpdateRef.current = timestamp; // Spawn collectibles
frameRef.current = requestAnimationFrame(gameLoop); spawnCollectible();
};
frameRef.current = requestAnimationFrame(gameLoop); // Check collisions
const { player, enemies, collectibles } = gameStore;
return () => {
if (frameRef.current) { // Enemy collisions
cancelAnimationFrame(frameRef.current); enemies.forEach((enemy) => {
} const dx = player.position.x - enemy.position.x;
}; const dy = player.position.y - enemy.position.y;
}, []); const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5 && !player.isInvincible) {
gameStore.takeDamage(20);
}
});
// Collectible collisions
collectibles.forEach((collectible) => {
const dx = player.position.x - collectible.position.x;
const dy = player.position.y - collectible.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5) {
gameStore.collectItem(collectible.id);
}
});
}
lastUpdateRef.current = timestamp;
frameRef.current = requestAnimationFrame(gameLoop);
};
frameRef.current = requestAnimationFrame(gameLoop);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, []);
}; };

View file

@ -1,129 +1,144 @@
import { create } from 'zustand'; import { create } from "zustand";
import type { GameState, Position } from '@/types/game'; import type { GameState, Position } from "@/types/game";
const useGameStore = create<GameState>((set, get) => ({ const useGameStore = create<GameState>((set, get) => ({
player: {
position: { x: 50, y: 50 },
health: 100,
speed: 5,
powerUps: [],
isInvincible: false,
hasKey: false
},
enemies: [],
collectibles: [],
powerUps: [],
score: 0,
level: 1,
gameStatus: 'MENU',
highScores: [],
timePlayed: 0,
movePlayer: (direction: Position) => {
const { player } = get();
set({
player: {
...player,
position: {
x: Math.max(0, Math.min(100, player.position.x + direction.x * player.speed)),
y: Math.max(0, Math.min(100, player.position.y + direction.y * player.speed))
}
}
});
},
updateEnemies: () => {
const { enemies, player } = get();
const updatedEnemies = enemies.map(enemy => {
const dx = player.position.x - enemy.position.x;
const dy = player.position.y - enemy.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return {
...enemy,
direction: {
x: dx / distance,
y: dy / distance
},
position: {
x: enemy.position.x + (enemy.direction.x * enemy.speed),
y: enemy.position.y + (enemy.direction.y * enemy.speed)
}
};
});
set({ enemies: updatedEnemies });
},
collectItem: (itemId: string) => {
const { collectibles, score } = get();
const item = collectibles.find(c => c.id === itemId);
if (!item) return;
set({
collectibles: collectibles.filter(c => c.id !== itemId),
score: score + item.value
});
},
takeDamage: (amount: number) => {
const { player, gameStatus } = get();
if (player.isInvincible) return;
const newHealth = player.health - amount;
set({
player: {
...player,
health: newHealth
},
gameStatus: newHealth <= 0 ? 'GAME_OVER' : gameStatus
});
},
activatePowerUp: (powerUpId: string) => {
const { player, powerUps } = get();
const powerUp = powerUps.find(p => p.id === powerUpId);
if (!powerUp) return;
set({
player: {
...player,
powerUps: [...player.powerUps, powerUp]
},
powerUps: powerUps.filter(p => p.id !== powerUpId)
});
setTimeout(() => {
const currentPlayer = get().player;
set({
player: {
...currentPlayer,
powerUps: currentPlayer.powerUps.filter(p => p.id !== powerUp.id)
}
});
}, powerUp.duration);
},
startNewGame: () => set({
player: { player: {
position: { x: 50, y: 50 }, position: { x: 50, y: 50 },
health: 100, health: 100,
speed: 5, speed: 5,
powerUps: [], powerUps: [],
isInvincible: false, isInvincible: false,
hasKey: false hasKey: false,
}, },
enemies: [], enemies: [],
collectibles: [], collectibles: [],
powerUps: [], powerUps: [],
score: 0, score: 0,
level: 1, level: 1,
gameStatus: 'PLAYING', gameStatus: "MENU",
timePlayed: 0 highScores: [],
}), timePlayed: 0,
pauseGame: () => set({ gameStatus: 'PAUSED' }), movePlayer: (direction: Position) => {
resumeGame: () => set({ gameStatus: 'PLAYING' }) const { player } = get();
set({
player: {
...player,
position: {
x: Math.max(
0,
Math.min(
100,
player.position.x + direction.x * player.speed,
),
),
y: Math.max(
0,
Math.min(
100,
player.position.y + direction.y * player.speed,
),
),
},
},
});
},
updateEnemies: () => {
const { enemies, player } = get();
const updatedEnemies = enemies.map((enemy) => {
const dx = player.position.x - enemy.position.x;
const dy = player.position.y - enemy.position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return {
...enemy,
direction: {
x: dx / distance,
y: dy / distance,
},
position: {
x: enemy.position.x + enemy.direction.x * enemy.speed,
y: enemy.position.y + enemy.direction.y * enemy.speed,
},
};
});
set({ enemies: updatedEnemies });
},
collectItem: (itemId: string) => {
const { collectibles, score } = get();
const item = collectibles.find((c) => c.id === itemId);
if (!item) return;
set({
collectibles: collectibles.filter((c) => c.id !== itemId),
score: score + item.value,
});
},
takeDamage: (amount: number) => {
const { player, gameStatus } = get();
if (player.isInvincible) return;
const newHealth = player.health - amount;
set({
player: {
...player,
health: newHealth,
},
gameStatus: newHealth <= 0 ? "GAME_OVER" : gameStatus,
});
},
activatePowerUp: (powerUpId: string) => {
const { player, powerUps } = get();
const powerUp = powerUps.find((p) => p.id === powerUpId);
if (!powerUp) return;
set({
player: {
...player,
powerUps: [...player.powerUps, powerUp],
},
powerUps: powerUps.filter((p) => p.id !== powerUpId),
});
setTimeout(() => {
const currentPlayer = get().player;
set({
player: {
...currentPlayer,
powerUps: currentPlayer.powerUps.filter(
(p) => p.id !== powerUp.id,
),
},
});
}, powerUp.duration);
},
startNewGame: () =>
set({
player: {
position: { x: 50, y: 50 },
health: 100,
speed: 5,
powerUps: [],
isInvincible: false,
hasKey: false,
},
enemies: [],
collectibles: [],
powerUps: [],
score: 0,
level: 1,
gameStatus: "PLAYING",
timePlayed: 0,
}),
pauseGame: () => set({ gameStatus: "PAUSED" }),
resumeGame: () => set({ gameStatus: "PLAYING" }),
})); }));
export default useGameStore; export default useGameStore;

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import type { GithubRepo } from '@/types'; import type { GithubRepo } from "@/types";
const useGithubRepos = () => { const useGithubRepos = () => {
const [repos, setRepos] = useState<GithubRepo[]>([]); const [repos, setRepos] = useState<GithubRepo[]>([]);
@ -9,37 +9,48 @@ const useGithubRepos = () => {
useEffect(() => { useEffect(() => {
const fetchRepos = async () => { const fetchRepos = async () => {
try { try {
const response = await fetch('https://api.github.com/users/EndofTimee/repos?sort=updated'); const response = await fetch(
"https://api.github.com/users/EndofTimee/repos?sort=updated",
);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch repositories'); throw new Error("Failed to fetch repositories");
} }
const reposData = await response.json() as GithubRepo[]; const reposData = (await response.json()) as GithubRepo[];
const repoDetails = await Promise.all( const repoDetails = await Promise.all(
reposData.map(async (repo: GithubRepo) => { reposData.map(async (repo: GithubRepo) => {
try { try {
const languagesResponse = await fetch(repo.languages_url); const languagesResponse = await fetch(
const languages = await languagesResponse.json() as Record<string, number>; repo.languages_url,
);
const languages =
(await languagesResponse.json()) as Record<
string,
number
>;
return { return {
...repo, ...repo,
languages: Object.keys(languages) languages: Object.keys(languages),
}; };
} catch (error) { } catch (error) {
console.error(`Error fetching languages for ${repo.name}:`, error); console.error(
`Error fetching languages for ${repo.name}:`,
error,
);
return { return {
...repo, ...repo,
languages: [] languages: [],
}; };
} }
}) }),
); );
setRepos(repoDetails); setRepos(repoDetails);
setError(null); setError(null);
} catch (err) { } catch (err) {
const error = err as Error; const error = err as Error;
setError(error.message); setError(error.message);
console.error('Error fetching repos:', err); console.error("Error fetching repos:", err);
} finally { } finally {
setLoading(false); setLoading(false);
} }

View file

@ -1,10 +1,10 @@
import React from 'react' import React from "react";
import ReactDOM from 'react-dom/client' import ReactDOM from "react-dom/client";
import App from '@/App' import App from "@/App";
import '@/styles/index.css' import "@/styles/index.css";
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode>, </React.StrictMode>,
) );

View file

@ -1,12 +1,14 @@
import FoxCard from '@/components/FoxCard'; import FoxCard from "@/components/FoxCard";
import { Code, Cpu } from 'lucide-react'; import { Code, Cpu } from "lucide-react";
const APCSPPage = () => { const APCSPPage = () => {
return ( return (
<div className="page-container"> <div className="page-container">
<FoxCard className="header-card"> <FoxCard className="header-card">
<h1 className="text-glow">AP Computer Science Principles</h1> <h1 className="text-glow">AP Computer Science Principles</h1>
<p className="text-gradient">Exploring the foundations of modern computing</p> <p className="text-gradient">
Exploring the foundations of modern computing
</p>
</FoxCard> </FoxCard>
<div className="content-grid"> <div className="content-grid">
@ -15,7 +17,10 @@ const APCSPPage = () => {
<Code size={24} className="text-accent-primary" /> <Code size={24} className="text-accent-primary" />
<h2>Programming Concepts</h2> <h2>Programming Concepts</h2>
</div> </div>
<p>Learn the creative aspects of programming, abstractions, and algorithms</p> <p>
Learn the creative aspects of programming, abstractions,
and algorithms
</p>
</FoxCard> </FoxCard>
<FoxCard> <FoxCard>
@ -24,8 +29,8 @@ const APCSPPage = () => {
<h2>Project Demo</h2> <h2>Project Demo</h2>
</div> </div>
<div className="project-demo"> <div className="project-demo">
<iframe <iframe
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview" src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
width="100%" width="100%"
height="400" height="400"
className="rounded-lg" className="rounded-lg"

View file

@ -1,26 +1,26 @@
import { Gamepad2, Code, Music } from 'lucide-react'; import { Gamepad2, Code, Music } from "lucide-react";
import { useState, useEffect } from 'react'; import { useState, useEffect } from "react";
import FoxCard from '@/components/FoxCard'; import FoxCard from "@/components/FoxCard";
import MusicDisplay from '@/components/MusicDisplay'; import MusicDisplay from "@/components/MusicDisplay";
import { calculatePreciseAge } from '@/utils/dateUtils'; import { calculatePreciseAge } from "@/utils/dateUtils";
const AboutPage = () => { const AboutPage = () => {
const [age, setAge] = useState(calculatePreciseAge(new Date("2009-05-15"))); // const [age, setAge] = useState(calculatePreciseAge(new Date("date")));
useEffect(() => { // useEffect(() => {
const interval = setInterval(() => { // const interval = setInterval(() => {
setAge(calculatePreciseAge(new Date("2009-05-15"))); // setAge(calculatePreciseAge(new Date("date")));
}, 50); // }, 50);
return () => clearInterval(interval); // return () => clearInterval(interval);
}, []); // }, []);
return ( return (
<div className="page-container"> <div className="page-container">
<FoxCard className="header-card"> <FoxCard className="header-card">
<h1 className="text-glow">About Me</h1> <h1 className="text-glow">About Me</h1>
<p className="text-gradient"> <p className="text-gradient">
End ProtoFoxes They/Them {age} years old Programmer & Streamer End They/It/She Programmer & Streamer
</p> </p>
</FoxCard> </FoxCard>
@ -43,16 +43,16 @@ const AboutPage = () => {
<h2>Streaming</h2> <h2>Streaming</h2>
</div> </div>
<p> <p>
Find me on{' '} Find me on{" "}
<a <a
href="https://twitch.tv/EndofTimee" href="https://twitch.tv/EndofTimee"
className="text-accent-neon hover:text-glow" className="text-accent-neon hover:text-glow"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
Twitch Twitch
</a> </a>{" "}
{' '}playing FiveM and other games! playing FiveM and other games!
</p> </p>
</FoxCard> </FoxCard>
@ -68,4 +68,4 @@ const AboutPage = () => {
); );
}; };
export default AboutPage; export default AboutPage;

View file

@ -1,7 +1,7 @@
import FoxCard from '@/components/FoxCard'; import FoxCard from "@/components/FoxCard";
import GithubRepos from '@/components/GithubRepos'; import GithubRepos from "@/components/GithubRepos";
import useGithubRepos from '@/hooks/useGithubRepos'; import useGithubRepos from "@/hooks/useGithubRepos";
import LoadingFox from '@/components/LoadingFox'; import LoadingFox from "@/components/LoadingFox";
const ProjectsPage = () => { const ProjectsPage = () => {
const { repos, loading, error } = useGithubRepos(); const { repos, loading, error } = useGithubRepos();
@ -10,7 +10,9 @@ const ProjectsPage = () => {
<div className="page-container"> <div className="page-container">
<FoxCard className="header-card"> <FoxCard className="header-card">
<h1 className="text-glow">My Projects</h1> <h1 className="text-glow">My Projects</h1>
<p className="text-gradient">Exploring code, one repo at a time</p> <p className="text-gradient">
Exploring code, one repo at a time
</p>
</FoxCard> </FoxCard>
{loading ? ( {loading ? (
@ -18,7 +20,10 @@ const ProjectsPage = () => {
) : error ? ( ) : error ? (
<FoxCard className="error-card"> <FoxCard className="error-card">
<p>Oops! Something went wrong fetching the repositories.</p> <p>Oops! Something went wrong fetching the repositories.</p>
<button onClick={() => window.location.reload()} className="retry-button"> <button
onClick={() => window.location.reload()}
className="retry-button"
>
Try Again Try Again
</button> </button>
</FoxCard> </FoxCard>

View file

@ -1,15 +1,17 @@
const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => { const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => {
if (onPerfEntry && typeof onPerfEntry === 'function') { if (onPerfEntry && typeof onPerfEntry === "function") {
import('web-vitals').then((vitals) => { import("web-vitals")
const { onCLS, onFID, onFCP, onLCP, onTTFB } = vitals; .then((vitals) => {
onCLS(onPerfEntry); const { onCLS, onFID, onFCP, onLCP, onTTFB } = vitals;
onFID(onPerfEntry); onCLS(onPerfEntry);
onFCP(onPerfEntry); onFID(onPerfEntry);
onLCP(onPerfEntry); onFCP(onPerfEntry);
onTTFB(onPerfEntry); onLCP(onPerfEntry);
}).catch((error) => { onTTFB(onPerfEntry);
console.error('Error loading web-vitals:', error); })
}); .catch((error) => {
console.error("Error loading web-vitals:", error);
});
} }
}; };

View file

@ -1,138 +1,142 @@
/* src/App.css */ /* src/App.css */
:root { :root {
--background-primary: #1a0b2e; --background-primary: #1a0b2e;
--background-secondary: #2f1c54; --background-secondary: #2f1c54;
--accent-primary: #9d4edd; --accent-primary: #9d4edd;
--accent-neon: #b249f8; --accent-neon: #b249f8;
--text-glow: #e0aaff; --text-glow: #e0aaff;
--text-primary: #ffffff; --text-primary: #ffffff;
--dark-accent: #240046; --dark-accent: #240046;
} }
/* Custom Scrollbar */ /* Custom Scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: var(--background-primary); background: var(--background-primary);
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: var(--accent-primary); background: var(--accent-primary);
border-radius: 5px; border-radius: 5px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--accent-neon); background: var(--accent-neon);
box-shadow: 0 0 10px var(--text-glow); box-shadow: 0 0 10px var(--text-glow);
} }
/* Particle Effects */ /* Particle Effects */
.particle-container { .particle-container {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
} }
.particle { .particle {
position: absolute; position: absolute;
width: 3px; width: 3px;
height: 3px; height: 3px;
background: var(--text-glow); background: var(--text-glow);
border-radius: 50%; border-radius: 50%;
animation: particleFloat linear infinite; animation: particleFloat linear infinite;
opacity: 0.5; opacity: 0.5;
} }
@keyframes particleFloat { @keyframes particleFloat {
0% { 0% {
transform: translateY(100vh) scale(0); transform: translateY(100vh) scale(0);
opacity: 0; opacity: 0;
} }
50% { 50% {
opacity: 0.5; opacity: 0.5;
} }
100% { 100% {
transform: translateY(-20vh) scale(1); transform: translateY(-20vh) scale(1);
opacity: 0; opacity: 0;
} }
} }
/* Main Layout */ /* Main Layout */
.app-container { .app-container {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, var(--background-primary), var(--background-secondary)); background: linear-gradient(
color: var(--text-primary); 135deg,
font-family: 'Inter', sans-serif; var(--background-primary),
position: relative; var(--background-secondary)
overflow-x: hidden; );
color: var(--text-primary);
font-family: "Inter", sans-serif;
position: relative;
overflow-x: hidden;
} }
/* Header Styles */ /* Header Styles */
.header { .header {
text-align: center; text-align: center;
padding: 4rem 2rem; padding: 4rem 2rem;
} }
.header h1 { .header h1 {
font-size: 3.5rem; font-size: 3.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.subtitle { .subtitle {
font-size: 1.2rem; font-size: 1.2rem;
opacity: 0.9; opacity: 0.9;
} }
/* Content Sections */ /* Content Sections */
.content-section { .content-section {
max-width: 1200px; max-width: 1200px;
margin: 2rem auto; margin: 2rem auto;
padding: 2rem; padding: 2rem;
background: rgba(26, 11, 46, 0.5); background: rgba(26, 11, 46, 0.5);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 16px; border-radius: 16px;
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
/* Interests Grid */ /* Interests Grid */
.interests-grid { .interests-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem; gap: 2rem;
margin-top: 2rem; margin-top: 2rem;
} }
.interest-card { .interest-card {
background: rgba(47, 28, 84, 0.3); background: rgba(47, 28, 84, 0.3);
padding: 1.5rem; padding: 1.5rem;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(157, 78, 221, 0.2); border: 1px solid rgba(157, 78, 221, 0.2);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.interest-card:hover { .interest-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
border-color: var(--accent-neon); border-color: var(--accent-neon);
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2); box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
} }
/* Twitch Button */ /* Twitch Button */
.twitch-button { .twitch-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
background: #9146ff; background: #9146ff;
color: white; color: white;
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
border-radius: 8px; border-radius: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.twitch-button:hover { .twitch-button:hover {
background: #7c2bff; background: #7c2bff;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 0 15px rgba(145, 70, 255, 0.5); box-shadow: 0 0 15px rgba(145, 70, 255, 0.5);
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.header h1 { .header h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
.content-section { .content-section {
padding: 1rem; padding: 1rem;
margin: 1rem; margin: 1rem;
} }
.interests-grid { .interests-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }

View file

@ -1,523 +1,626 @@
/* Full screen overlay */ /* Full screen overlay */
.endos-boot-container { .endos-boot-container {
position: fixed; position: fixed;
inset: 0; inset: 0;
background-color: #000000; background-color: #000000;
z-index: 9999; z-index: 9999;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-family: 'Orbitron', monospace; font-family: "Orbitron", monospace;
color: #00E5FF; color: #00e5ff;
overflow: hidden; overflow: hidden;
} }
/* Scanning animation effect */ /* Scanning animation effect */
.boot-scan-line { .boot-scan-line {
position: absolute; position: absolute;
left: 0; left: 0;
width: 100%; width: 100%;
height: 4px; height: 4px;
background: linear-gradient(90deg, background: linear-gradient(
rgba(0, 229, 255, 0) 0%, 90deg,
rgba(0, 229, 255, 0.8) 50%, rgba(0, 229, 255, 0) 0%,
rgba(0, 229, 255, 0) 100%); rgba(0, 229, 255, 0.8) 50%,
box-shadow: 0 0 10px rgba(0, 229, 255, 0.8); rgba(0, 229, 255, 0) 100%
animation: scanAnimation 3s linear infinite; );
pointer-events: none; box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
animation: scanAnimation 3s linear infinite;
pointer-events: none;
} }
@keyframes scanAnimation { @keyframes scanAnimation {
0% { top: -10px; } 0% {
100% { top: 100vh; } top: -10px;
}
100% {
top: 100vh;
}
} }
/* Fox ear decorations */ /* Fox ear decorations */
.visor-left-ear, .visor-left-ear,
.visor-right-ear { .visor-right-ear {
position: absolute; position: absolute;
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: #ff9466; background-color: #ff9466;
top: -20px; top: -20px;
z-index: 2; z-index: 2;
} }
.visor-left-ear { .visor-left-ear {
left: calc(50% - 100px); left: calc(50% - 100px);
transform: rotate(45deg); transform: rotate(45deg);
border-radius: 0 0 0 20px; border-radius: 0 0 0 20px;
transform-origin: bottom right; transform-origin: bottom right;
animation: earTwitch 4s ease-in-out infinite; animation: earTwitch 4s ease-in-out infinite;
} }
.visor-right-ear { .visor-right-ear {
right: calc(50% - 100px); right: calc(50% - 100px);
transform: rotate(-45deg); transform: rotate(-45deg);
border-radius: 0 0 20px 0; border-radius: 0 0 20px 0;
transform-origin: bottom left; transform-origin: bottom left;
animation: earTwitchRight 4s ease-in-out infinite; animation: earTwitchRight 4s ease-in-out infinite;
} }
@keyframes earTwitch { @keyframes earTwitch {
0%, 100% { transform: rotate(45deg); } 0%,
50% { transform: rotate(30deg); } 100% {
transform: rotate(45deg);
}
50% {
transform: rotate(30deg);
}
} }
@keyframes earTwitchRight { @keyframes earTwitchRight {
0%, 100% { transform: rotate(-45deg); } 0%,
50% { transform: rotate(-30deg); } 100% {
transform: rotate(-45deg);
}
50% {
transform: rotate(-30deg);
}
} }
/* Main visor frame */ /* Main visor frame */
.boot-visor-frame { .boot-visor-frame {
position: relative; position: relative;
width: 80%; width: 80%;
max-width: 800px; max-width: 800px;
height: 60vh; height: 60vh;
max-height: 600px; max-height: 600px;
border: 2px solid #00E5FF; border: 2px solid #00e5ff;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5); box-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
overflow: hidden; overflow: hidden;
background-color: rgba(0, 10, 15, 0.95); background-color: rgba(0, 10, 15, 0.95);
} }
.boot-visor { .boot-visor {
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
overflow: hidden; overflow: hidden;
} }
/* Visor top and bottom glowing lines */ /* Visor top and bottom glowing lines */
.visor-line { .visor-line {
position: absolute; position: absolute;
left: 0; left: 0;
width: 100%; width: 100%;
height: 3px; height: 3px;
background-color: #00E5FF; background-color: #00e5ff;
box-shadow: 0 0 10px #00E5FF; box-shadow: 0 0 10px #00e5ff;
animation: glowPulse 2s ease-in-out infinite; animation: glowPulse 2s ease-in-out infinite;
} }
.visor-line.top { .visor-line.top {
top: 0; top: 0;
} }
.visor-line.bottom { .visor-line.bottom {
bottom: 0; bottom: 0;
} }
@keyframes glowPulse { @keyframes glowPulse {
0%, 100% { opacity: 1; box-shadow: 0 0 10px #00E5FF, 0 0 20px rgba(0, 229, 255, 0.5); } 0%,
50% { opacity: 0.7; box-shadow: 0 0 15px #00E5FF, 0 0 30px rgba(0, 229, 255, 0.7); } 100% {
opacity: 1;
box-shadow:
0 0 10px #00e5ff,
0 0 20px rgba(0, 229, 255, 0.5);
}
50% {
opacity: 0.7;
box-shadow:
0 0 15px #00e5ff,
0 0 30px rgba(0, 229, 255, 0.7);
}
} }
/* Boot content area */ /* Boot content area */
.boot-content { .boot-content {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
opacity: 0; opacity: 0;
transition: opacity 0.5s ease; transition: opacity 0.5s ease;
transform: translateY(0); /* Reset to prevent z-index conflicts */ transform: translateY(0); /* Reset to prevent z-index conflicts */
} }
.boot-content.active { .boot-content.active {
opacity: 1; opacity: 1;
} }
/* Common styles for all boot stages */ /* Common styles for all boot stages */
.boot-stage { .boot-stage {
position: absolute; position: absolute;
width: 80%; width: 80%;
max-width: 600px; max-width: 600px;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
background-color: rgba(0, 0, 0, 0.6); /* Darker background to prevent seeing through */ background-color: rgba(
border: 1px solid rgba(0, 229, 255, 0.3); 0,
border-radius: 5px; 0,
opacity: 0; 0,
visibility: hidden; 0.6
display: none; /* Added display:none to completely remove from flow */ ); /* Darker background to prevent seeing through */
transition: opacity 0.3s ease, visibility 0.3s ease; border: 1px solid rgba(0, 229, 255, 0.3);
max-height: 80%; border-radius: 5px;
overflow-y: auto; opacity: 0;
z-index: 1; /* Ensure z-index is consistent */ visibility: hidden;
display: none; /* Added display:none to completely remove from flow */
transition:
opacity 0.3s ease,
visibility 0.3s ease;
max-height: 80%;
overflow-y: auto;
z-index: 1; /* Ensure z-index is consistent */
} }
.boot-stage.active { .boot-stage.active {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
display: block; /* Make it visible in the flow */ display: block; /* Make it visible in the flow */
z-index: 10; /* Higher z-index when active */ z-index: 10; /* Higher z-index when active */
} }
/* Hide scrollbar but allow scrolling */ /* Hide scrollbar but allow scrolling */
.boot-stage::-webkit-scrollbar { .boot-stage::-webkit-scrollbar {
width: 3px; width: 3px;
} }
.boot-stage::-webkit-scrollbar-track { .boot-stage::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
} }
.boot-stage::-webkit-scrollbar-thumb { .boot-stage::-webkit-scrollbar-thumb {
background: rgba(0, 229, 255, 0.3); background: rgba(0, 229, 255, 0.3);
} }
/* BIOS Stage */ /* BIOS Stage */
.bios-header { .bios-header {
font-size: 24px; font-size: 24px;
margin-bottom: 20px; margin-bottom: 20px;
color: #FF005C; color: #ff005c;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7); text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
} }
.boot-text-line { .boot-text-line {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 14px; font-size: 14px;
margin: 8px 0; margin: 8px 0;
color: #8be9fd; color: #8be9fd;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
animation: typeWriter 1s steps(50, end); animation: typeWriter 1s steps(50, end);
} }
@keyframes typeWriter { @keyframes typeWriter {
from { width: 0; } from {
to { width: 100%; } width: 0;
}
to {
width: 100%;
}
} }
/* System Scan Stage */ /* System Scan Stage */
.scan-header { .scan-header {
font-size: 20px; font-size: 20px;
margin-bottom: 15px; margin-bottom: 15px;
color: #FF005C; color: #ff005c;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7); text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
} }
.scan-progress-container { .scan-progress-container {
width: 100%; width: 100%;
height: 10px; height: 10px;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 229, 255, 0.5); border: 1px solid rgba(0, 229, 255, 0.5);
border-radius: 5px; border-radius: 5px;
margin: 20px 0; margin: 20px 0;
overflow: hidden; overflow: hidden;
} }
.scan-progress-bar { .scan-progress-bar {
height: 100%; height: 100%;
background: linear-gradient(90deg, #00E5FF, #FF005C); background: linear-gradient(90deg, #00e5ff, #ff005c);
width: 0; width: 0;
animation: progressFill 2s ease-in-out forwards; animation: progressFill 2s ease-in-out forwards;
} }
@keyframes progressFill { @keyframes progressFill {
0% { width: 0; } 0% {
100% { width: 100%; } width: 0;
}
100% {
width: 100%;
}
} }
.scan-detail { .scan-detail {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 14px; font-size: 14px;
margin: 8px 0; margin: 8px 0;
color: #8be9fd; color: #8be9fd;
} }
/* Module Loading Stage */ /* Module Loading Stage */
.module-header { .module-header {
font-size: 20px; font-size: 20px;
margin-bottom: 15px; margin-bottom: 15px;
color: #FF005C; color: #ff005c;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7); text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
} }
.modules-grid { .modules-grid {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 15px; gap: 15px;
margin-top: 20px; margin-top: 20px;
} }
.module-item { .module-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 10px; padding: 10px;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 229, 255, 0.2); border: 1px solid rgba(0, 229, 255, 0.2);
border-radius: 5px; border-radius: 5px;
animation: moduleLoad 1s ease forwards; animation: moduleLoad 1s ease forwards;
opacity: 0; opacity: 0;
} }
.module-item:nth-child(1) { animation-delay: 0.2s; } .module-item:nth-child(1) {
.module-item:nth-child(2) { animation-delay: 0.4s; } animation-delay: 0.2s;
.module-item:nth-child(3) { animation-delay: 0.6s; } }
.module-item:nth-child(4) { animation-delay: 0.8s; } .module-item:nth-child(2) {
animation-delay: 0.4s;
}
.module-item:nth-child(3) {
animation-delay: 0.6s;
}
.module-item:nth-child(4) {
animation-delay: 0.8s;
}
@keyframes moduleLoad { @keyframes moduleLoad {
0% { opacity: 0; transform: translateX(-20px); } 0% {
100% { opacity: 1; transform: translateX(0); } opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
} }
.module-icon { .module-icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
border-radius: 50%; border-radius: 50%;
background-color: #00E5FF; background-color: #00e5ff;
margin-right: 10px; margin-right: 10px;
animation: iconPulse 2s infinite; animation: iconPulse 2s infinite;
} }
@keyframes iconPulse { @keyframes iconPulse {
0%, 100% { transform: scale(1); box-shadow: 0 0 5px #00E5FF; } 0%,
50% { transform: scale(1.1); box-shadow: 0 0 10px #00E5FF; } 100% {
transform: scale(1);
box-shadow: 0 0 5px #00e5ff;
}
50% {
transform: scale(1.1);
box-shadow: 0 0 10px #00e5ff;
}
} }
.module-name { .module-name {
font-size: 14px; font-size: 14px;
color: #fff; color: #fff;
} }
/* Fox Protocol Stage */ /* Fox Protocol Stage */
.fox-header { .fox-header {
font-size: 20px; font-size: 20px;
margin-bottom: 15px; margin-bottom: 15px;
color: #ff9466; color: #ff9466;
text-align: center; text-align: center;
text-shadow: 0 0 10px rgba(255, 148, 102, 0.7); text-shadow: 0 0 10px rgba(255, 148, 102, 0.7);
} }
.fox-trait { .fox-trait {
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
font-size: 14px; font-size: 14px;
margin: 10px 0; margin: 10px 0;
color: #ff9466; color: #ff9466;
padding-left: 20px; padding-left: 20px;
position: relative; position: relative;
opacity: 0; opacity: 0;
animation: traitAppear 0.5s ease forwards; animation: traitAppear 0.5s ease forwards;
} }
.fox-trait:nth-child(2) { animation-delay: 0.2s; } .fox-trait:nth-child(2) {
.fox-trait:nth-child(3) { animation-delay: 0.4s; } animation-delay: 0.2s;
.fox-trait:nth-child(4) { animation-delay: 0.6s; } }
.fox-trait:nth-child(5) { animation-delay: 0.8s; } .fox-trait:nth-child(3) {
.fox-trait:nth-child(6) { animation-delay: 1s; } animation-delay: 0.4s;
}
.fox-trait:nth-child(4) {
animation-delay: 0.6s;
}
.fox-trait:nth-child(5) {
animation-delay: 0.8s;
}
.fox-trait:nth-child(6) {
animation-delay: 1s;
}
@keyframes traitAppear { @keyframes traitAppear {
0% { opacity: 0; transform: translateX(-10px); } 0% {
100% { opacity: 1; transform: translateX(0); } opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
} }
.fox-trait::before { .fox-trait::before {
content: '>'; content: ">";
position: absolute; position: absolute;
left: 0; left: 0;
color: #ff9466; color: #ff9466;
} }
/* Logo Display Stage */ /* Logo Display Stage */
.logo-display { .logo-display {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.endos-logo { .endos-logo {
font-size: 48px; font-size: 48px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
text-align: center; text-align: center;
background: linear-gradient(90deg, #ff9466, #00E5FF); background: linear-gradient(90deg, #ff9466, #00e5ff);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
text-fill-color: transparent; text-fill-color: transparent;
text-shadow: 0 0 20px rgba(0, 229, 255, 0.5); text-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
opacity: 0; opacity: 0;
animation: logoAppear 1s ease forwards; animation: logoAppear 1s ease forwards;
} }
.logo-end { .logo-end {
color: #ff9466; color: #ff9466;
} }
.logo-os { .logo-os {
color: #00E5FF; color: #00e5ff;
} }
.logo-subtitle { .logo-subtitle {
font-size: 16px; font-size: 16px;
color: #fff; color: #fff;
opacity: 0; opacity: 0;
animation: logoAppear 1s ease forwards 0.5s; animation: logoAppear 1s ease forwards 0.5s;
} }
@keyframes logoAppear { @keyframes logoAppear {
0% { opacity: 0; transform: scale(0.9); } 0% {
100% { opacity: 1; transform: scale(1); } opacity: 0;
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
} }
/* System Ready Stage */ /* System Ready Stage */
.system-ready { .system-ready {
text-align: center; text-align: center;
} }
.ready-status { .ready-status {
font-size: 28px; font-size: 28px;
margin-bottom: 15px; margin-bottom: 15px;
color: #00E5FF; color: #00e5ff;
animation: textPulse 2s infinite; animation: textPulse 2s infinite;
} }
@keyframes textPulse { @keyframes textPulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.7; } 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
} }
.welcome-message { .welcome-message {
font-size: 18px; font-size: 18px;
margin-bottom: 5px; margin-bottom: 5px;
color: #fff; color: #fff;
} }
.boot-complete-message { .boot-complete-message {
font-size: 14px; font-size: 14px;
color: #8be9fd; color: #8be9fd;
margin-top: 20px; margin-top: 20px;
} }
/* Skip button - Enhanced with animation and more prominence */ /* Skip button - Enhanced with animation and more prominence */
.skip-button { .skip-button {
position: absolute; position: absolute;
bottom: 40px; /* More visible position */ bottom: 40px; /* More visible position */
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background-color: rgba(255, 0, 92, 0.3); /* More visible color */ background-color: rgba(255, 0, 92, 0.3); /* More visible color */
color: white; color: white;
border: 1px solid rgba(255, 0, 92, 0.5); border: 1px solid rgba(255, 0, 92, 0.5);
border-radius: 5px; border-radius: 5px;
padding: 10px 20px; /* Larger size */ padding: 10px 20px; /* Larger size */
font-family: 'Orbitron', sans-serif; font-family: "Orbitron", sans-serif;
font-size: 14px; /* Larger font */ font-size: 14px; /* Larger font */
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
z-index: 100; z-index: 100;
animation: skipPulse 2s infinite; /* Pulsing animation to draw attention */ animation: skipPulse 2s infinite; /* Pulsing animation to draw attention */
box-shadow: 0 0 10px rgba(255, 0, 92, 0.3); /* Glow effect */ box-shadow: 0 0 10px rgba(255, 0, 92, 0.3); /* Glow effect */
} }
.skip-button:hover { .skip-button:hover {
background-color: rgba(255, 0, 92, 0.7); background-color: rgba(255, 0, 92, 0.7);
color: white; color: white;
border-color: rgba(255, 0, 92, 0.8); border-color: rgba(255, 0, 92, 0.8);
box-shadow: 0 0 15px rgba(255, 0, 92, 0.5); box-shadow: 0 0 15px rgba(255, 0, 92, 0.5);
transform: translateX(-50%) scale(1.05); transform: translateX(-50%) scale(1.05);
} }
@keyframes skipPulse { @keyframes skipPulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.7; } 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
} }
/* Accessibility */ /* Accessibility */
.visually-hidden { .visually-hidden {
position: absolute; position: absolute;
width: 1px; width: 1px;
height: 1px; height: 1px;
padding: 0; padding: 0;
margin: -1px; margin: -1px;
overflow: hidden; overflow: hidden;
clip: rect(0, 0, 0, 0); clip: rect(0, 0, 0, 0);
white-space: nowrap; white-space: nowrap;
border-width: 0; border-width: 0;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.boot-visor-frame { .boot-visor-frame {
width: 95%; width: 95%;
height: 80vh; height: 80vh;
} }
.visor-left-ear, .visor-right-ear { .visor-left-ear,
width: 30px; .visor-right-ear {
height: 30px; width: 30px;
top: -15px; height: 30px;
} top: -15px;
}
.visor-left-ear {
left: calc(50% - 70px); .visor-left-ear {
} left: calc(50% - 70px);
}
.visor-right-ear {
right: calc(50% - 70px); .visor-right-ear {
} right: calc(50% - 70px);
}
.boot-visor {
padding: 15px; .boot-visor {
} padding: 15px;
}
.boot-stage {
width: 90%; .boot-stage {
padding: 15px; width: 90%;
} padding: 15px;
}
.modules-grid {
grid-template-columns: 1fr; .modules-grid {
} grid-template-columns: 1fr;
}
.bios-header, .scan-header, .module-header, .fox-header {
font-size: 18px; .bios-header,
} .scan-header,
.module-header,
.endos-logo { .fox-header {
font-size: 36px; font-size: 18px;
} }
.logo-subtitle { .endos-logo {
font-size: 14px; font-size: 36px;
} }
.ready-status { .logo-subtitle {
font-size: 24px; font-size: 14px;
} }
.ready-status {
font-size: 24px;
}
} }
/* Very small screens */ /* Very small screens */
@media (max-width: 480px) { @media (max-width: 480px) {
.boot-stage { .boot-stage {
width: 95%; width: 95%;
padding: 10px; padding: 10px;
} }
.bios-header, .scan-header, .module-header, .fox-header { .bios-header,
font-size: 16px; .scan-header,
} .module-header,
.fox-header {
.boot-text-line, .scan-detail, .fox-trait { font-size: 16px;
font-size: 12px; }
}
.boot-text-line,
.skip-button { .scan-detail,
padding: 8px 16px; .fox-trait {
font-size: 12px; font-size: 12px;
} }
.skip-button {
padding: 8px 16px;
font-size: 12px;
}
} }

View file

@ -1,31 +1,31 @@
.fox-card { .fox-card {
position: relative; position: relative;
background: var(--gradient-card); background: var(--gradient-card);
border-radius: 16px; border-radius: 16px;
padding: 1.5rem; padding: 1.5rem;
border: 1px solid rgba(157, 78, 221, 0.2); border: 1px solid rgba(157, 78, 221, 0.2);
overflow: hidden; overflow: hidden;
} }
.fox-ear { .fox-ear {
position: absolute; position: absolute;
width: 30px; width: 30px;
height: 30px; height: 30px;
background: var(--fox-pink); background: var(--fox-pink);
opacity: 0.1; opacity: 0.1;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
.fox-ear-left { .fox-ear-left {
top: -15px; top: -15px;
left: -15px; left: -15px;
transform: rotate(45deg); transform: rotate(45deg);
border-radius: 0 0 0 15px; border-radius: 0 0 0 15px;
} }
.fox-ear-right { .fox-ear-right {
top: -15px; top: -15px;
right: -15px; right: -15px;
transform: rotate(-45deg); transform: rotate(-45deg);
border-radius: 0 0 15px 0; border-radius: 0 0 15px 0;
} }
.fox-card:hover .fox-ear { .fox-card:hover .fox-ear {
opacity: 0.2; opacity: 0.2;
} }

View file

@ -1,60 +1,60 @@
.github-repos-container { .github-repos-container {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
} }
.repos-grid { .repos-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem; gap: 1.5rem;
} }
.repo-card { .repo-card {
background: rgba(47, 28, 84, 0.3); background: rgba(47, 28, 84, 0.3);
border: 1px solid rgba(157, 78, 221, 0.2); border: 1px solid rgba(157, 78, 221, 0.2);
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.repo-card:hover { .repo-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
border-color: var(--accent-neon); border-color: var(--accent-neon);
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2); box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
} }
.repo-name { .repo-name {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
text-decoration: none; text-decoration: none;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
display: block; display: block;
} }
.repo-name:hover { .repo-name:hover {
color: var(--accent-neon); color: var(--accent-neon);
} }
.repo-description { .repo-description {
color: var(--text-primary); color: var(--text-primary);
opacity: 0.8; opacity: 0.8;
margin: 0.5rem 0; margin: 0.5rem 0;
font-size: 0.9rem; font-size: 0.9rem;
} }
.repo-language { .repo-language {
display: inline-block; display: inline-block;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
background: rgba(157, 78, 221, 0.2); background: rgba(157, 78, 221, 0.2);
border-radius: 1rem; border-radius: 1rem;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-primary); color: var(--text-primary);
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.repo-languages { .repo-languages {
margin-top: 0.5rem; margin-top: 0.5rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem; gap: 0.5rem;
} }
.language-tag { .language-tag {
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
background: rgba(157, 78, 221, 0.1); background: rgba(157, 78, 221, 0.1);
border-radius: 1rem; border-radius: 1rem;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-primary); color: var(--text-primary);
} }

View file

@ -1,42 +1,47 @@
.loading-fox-container { .loading-fox-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 200px; min-height: 200px;
} }
.fox-loader { .fox-loader {
position: relative; position: relative;
width: 100px; width: 100px;
height: 100px; height: 100px;
} }
.fox-face { .fox-face {
position: relative; position: relative;
width: 60px; width: 60px;
height: 60px; height: 60px;
background: var(--fox-orange); background: var(--fox-orange);
border-radius: 50%; border-radius: 50%;
animation: bounce 1s ease-in-out infinite; animation: bounce 1s ease-in-out infinite;
} }
.fox-ears { .fox-ears {
position: absolute; position: absolute;
top: -15px; top: -15px;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.ear { .ear {
width: 20px; width: 20px;
height: 20px; height: 20px;
background: var(--fox-orange); background: var(--fox-orange);
border-radius: 5px; border-radius: 5px;
} }
.ear.left { .ear.left {
transform: rotate(-30deg); transform: rotate(-30deg);
} }
.ear.right { .ear.right {
transform: rotate(30deg); transform: rotate(30deg);
} }
@keyframes bounce { @keyframes bounce {
0%, 100% { transform: translateY(0); } 0%,
50% { transform: translateY(-10px); } 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
} }

View file

@ -3,22 +3,22 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0.5rem; margin: 0.5rem;
background-color: rgba(26,11,46,0.5); background-color: rgba(26, 11, 46, 0.5);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
padding: 0.5rem; padding: 0.5rem;
border-radius: 1rem; border-radius: 1rem;
border: 1px solid rgb(157,78,221,0.2); border: 1px solid rgb(157, 78, 221, 0.2);
box-shadow: 0 4px 6px rgba(0,0,0,0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.theme-toggle-container:hover { .theme-toggle-container:hover {
border-color: rgba(157,78,221,0.4); border-color: rgba(157, 78, 221, 0.4);
box-shadow: 0 4px 12px rgba(157,78,221,0.2); box-shadow: 0 4px 12px rgba(157, 78, 221, 0.2);
} }
.theme-toggle { .theme-toggle {
position:relative; position: relative;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
@ -100,14 +100,14 @@ input:checked + .toggle-track .toggle-indicator {
} }
input:checked + .toggle-track .toggle.visor { input:checked + .toggle-track .toggle.visor {
background-color: #00E5FF; background-color: #00e5ff;
box-shadow: 0 0 5px #00e5FF; box-shadow: 0 0 5px #00e5ff;
} }
.toggle-label { .toggle-label {
margin-left: 10px; margin-left: 10px;
font-size: 14px; font-size: 14px;
transition: all 0.3s ease; transition: all 0.3s ease;
white-space: nowrap; white-space: nowrap;
} }
@ -128,8 +128,7 @@ input:checked + .toggle-track .toggle-label {
width: 50px; width: 50px;
} }
input:checked + .toggle-track .toggle-indicator { input:checked + .toggle-track .toggle-indicator {
left: 22px; left: 22px;
} }
} }

View file

@ -1,188 +1,267 @@
/* Basic Animations */ /* Basic Animations */
@keyframes float { @keyframes float {
0%, 100% { transform: translateY(0px); } 0%,
50% { transform: translateY(-10px); } 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.6; } 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
} }
@keyframes glow { @keyframes glow {
0%, 100% { filter: drop-shadow(0 0 2px var(--accent-neon)); } 0%,
50% { filter: drop-shadow(0 0 8px var(--accent-neon)); } 100% {
filter: drop-shadow(0 0 2px var(--accent-neon));
}
50% {
filter: drop-shadow(0 0 8px var(--accent-neon));
}
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
@keyframes bounce { @keyframes bounce {
0%, 100% { transform: translateY(0); } 0%,
50% { transform: translateY(-10px); } 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
@keyframes fadeOut { @keyframes fadeOut {
from { opacity: 1; transform: translateY(0); } from {
to { opacity: 0; transform: translateY(10px); } opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(10px);
}
} }
@keyframes slideInRight { @keyframes slideInRight {
from { transform: translateX(30px); opacity: 0; } from {
to { transform: translateX(0); opacity: 1; } transform: translateX(30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
} }
@keyframes slideInLeft { @keyframes slideInLeft {
from { transform: translateX(-30px); opacity: 0; } from {
to { transform: translateX(0); opacity: 1; } transform: translateX(-30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
} }
@keyframes wiggle { @keyframes wiggle {
0%, 100% { transform: rotate(0deg); } 0%,
25% { transform: rotate(5deg); } 100% {
75% { transform: rotate(-5deg); } transform: rotate(0deg);
}
25% {
transform: rotate(5deg);
}
75% {
transform: rotate(-5deg);
}
} }
/* System-specific animations */ /* System-specific animations */
@keyframes switchGlitch { @keyframes switchGlitch {
0% { transform: translate(0) skew(0); filter: hue-rotate(0deg); } 0% {
20% { transform: translate(-2px, 2px) skew(2deg, -2deg); filter: hue-rotate(90deg); } transform: translate(0) skew(0);
40% { transform: translate(2px, 0) skew(-2deg, 0); filter: hue-rotate(180deg); } filter: hue-rotate(0deg);
60% { transform: translate(0, -2px) skew(0, 2deg); filter: hue-rotate(270deg); } }
80% { transform: translate(-2px, 0) skew(2deg, 0); filter: hue-rotate(360deg); } 20% {
100% { transform: translate(0) skew(0); filter: hue-rotate(0deg); } transform: translate(-2px, 2px) skew(2deg, -2deg);
filter: hue-rotate(90deg);
}
40% {
transform: translate(2px, 0) skew(-2deg, 0);
filter: hue-rotate(180deg);
}
60% {
transform: translate(0, -2px) skew(0, 2deg);
filter: hue-rotate(270deg);
}
80% {
transform: translate(-2px, 0) skew(2deg, 0);
filter: hue-rotate(360deg);
}
100% {
transform: translate(0) skew(0);
filter: hue-rotate(0deg);
}
} }
@keyframes statusBlink { @keyframes statusBlink {
0%, 49% { opacity: 1; } 0%,
50%, 100% { opacity: 0.7; } 49% {
opacity: 1;
}
50%,
100% {
opacity: 0.7;
}
} }
/* Add animation classes */ /* Add animation classes */
.animate-float { .animate-float {
animation: float 3s ease-in-out infinite; animation: float 3s ease-in-out infinite;
} }
.animate-pulse { .animate-pulse {
animation: pulse 2s ease-in-out infinite; animation: pulse 2s ease-in-out infinite;
} }
.animate-glow { .animate-glow {
animation: glow 2s ease-in-out infinite; animation: glow 2s ease-in-out infinite;
} }
.animate-spin { .animate-spin {
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
.animate-bounce { .animate-bounce {
animation: bounce 1s ease-in-out infinite; animation: bounce 1s ease-in-out infinite;
} }
.animate-fade-in { .animate-fade-in {
animation: fadeIn 0.5s ease-in-out forwards; animation: fadeIn 0.5s ease-in-out forwards;
} }
.animate-fade-out { .animate-fade-out {
animation: fadeOut 0.5s ease-in-out forwards; animation: fadeOut 0.5s ease-in-out forwards;
} }
.animate-slide-in-right { .animate-slide-in-right {
animation: slideInRight 0.3s ease-out forwards; animation: slideInRight 0.3s ease-out forwards;
} }
.animate-slide-in-left { .animate-slide-in-left {
animation: slideInLeft 0.3s ease-out forwards; animation: slideInLeft 0.3s ease-out forwards;
} }
.animate-wiggle { .animate-wiggle {
animation: wiggle 1s ease-in-out; animation: wiggle 1s ease-in-out;
} }
.animate-switch-glitch { .animate-switch-glitch {
animation: switchGlitch 0.5s ease-in-out; animation: switchGlitch 0.5s ease-in-out;
} }
.animate-status-blink { .animate-status-blink {
animation: statusBlink 1s infinite; animation: statusBlink 1s infinite;
} }
/* Delay Utilities */ /* Delay Utilities */
.delay-100 { .delay-100 {
animation-delay: 100ms; animation-delay: 100ms;
} }
.delay-200 { .delay-200 {
animation-delay: 200ms; animation-delay: 200ms;
} }
.delay-300 { .delay-300 {
animation-delay: 300ms; animation-delay: 300ms;
} }
.delay-500 { .delay-500 {
animation-delay: 500ms; animation-delay: 500ms;
} }
.delay-700 { .delay-700 {
animation-delay: 700ms; animation-delay: 700ms;
} }
.delay-1000 { .delay-1000 {
animation-delay: 1000ms; animation-delay: 1000ms;
} }
/* Duration Utilities */ /* Duration Utilities */
.duration-300 { .duration-300 {
animation-duration: 300ms; animation-duration: 300ms;
} }
.duration-500 { .duration-500 {
animation-duration: 500ms; animation-duration: 500ms;
} }
.duration-700 { .duration-700 {
animation-duration: 700ms; animation-duration: 700ms;
} }
.duration-1000 { .duration-1000 {
animation-duration: 1000ms; animation-duration: 1000ms;
} }
.duration-2000 { .duration-2000 {
animation-duration: 2000ms; animation-duration: 2000ms;
} }
.duration-3000 { .duration-3000 {
animation-duration: 3000ms; animation-duration: 3000ms;
} }
/* Animation behavior */ /* Animation behavior */
.animation-once { .animation-once {
animation-iteration-count: 1; animation-iteration-count: 1;
} }
.animation-twice { .animation-twice {
animation-iteration-count: 2; animation-iteration-count: 2;
} }
.animation-infinite { .animation-infinite {
animation-iteration-count: infinite; animation-iteration-count: infinite;
} }
/* Pause animations when prefers-reduced-motion */ /* Pause animations when prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { * {
animation-duration: 0.01ms !important; animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important; animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important; transition-duration: 0.01ms !important;
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
} }

View file

@ -3,100 +3,104 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background-primary: #1a0b2e; --background-primary: #1a0b2e;
--background-secondary: #2f1c54; --background-secondary: #2f1c54;
--accent-primary: #9d4edd; --accent-primary: #9d4edd;
--accent-neon: #b249f8; --accent-neon: #b249f8;
--text-glow: #e0aaff; --text-glow: #e0aaff;
--text-primary: #ffffff; --text-primary: #ffffff;
--dark-accent: #240046; --dark-accent: #240046;
--fox-pink: #ffc6e5; --fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6; --fox-pink-glow: #ffadd6;
--fox-orange: #ff9466; --fox-orange: #ff9466;
--fox-white: #fff5f9; --fox-white: #fff5f9;
} }
body { body {
background-color: var(--background-primary); background-color: var(--background-primary);
color: var(--text-primary); color: var(--text-primary);
} }
} }
@layer utilities { @layer utilities {
.text-glow { .text-glow {
text-shadow: 0 0 10px var(--text-glow); text-shadow: 0 0 10px var(--text-glow);
} }
.section-spacing > * + * { .section-spacing > * + * {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.card-spacing > * + * { .card-spacing > * + * {
margin-top: 1rem; margin-top: 1rem;
} }
.element-spacing > * + * { .element-spacing > * + * {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
} }
@layer components { @layer components {
.content-wrapper { .content-wrapper {
max-width: 80rem; max-width: 80rem;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.nav-link:hover {
background-color: rgba(157, 78, 221, 0.05);
color: var(--accent-neon);
}
.nav-link.active {
background-color: rgba(157, 78, 221, 0.1);
color: var(--accent-neon);
}
.fox-card {
position: relative;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s ease;
border: 1px solid rgba(157, 78, 221, 0.1);
background: linear-gradient(135deg, rgba(47, 28, 84, 0.2) 0%, rgba(157, 78, 221, 0.05) 100%);
}
.fox-card:hover {
border-color: rgba(157, 78, 221, 0.2);
box-shadow: 0 2px 8px rgba(157, 78, 221, 0.05);
}
.content-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 768px) {
.content-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
} }
}
@media (min-width: 1024px) { .nav-link {
.content-grid { display: flex;
grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: center;
gap: 0.75rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
} }
}
} .nav-link:hover {
background-color: rgba(157, 78, 221, 0.05);
color: var(--accent-neon);
}
.nav-link.active {
background-color: rgba(157, 78, 221, 0.1);
color: var(--accent-neon);
}
.fox-card {
position: relative;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s ease;
border: 1px solid rgba(157, 78, 221, 0.1);
background: linear-gradient(
135deg,
rgba(47, 28, 84, 0.2) 0%,
rgba(157, 78, 221, 0.05) 100%
);
}
.fox-card:hover {
border-color: rgba(157, 78, 221, 0.2);
box-shadow: 0 2px 8px rgba(157, 78, 221, 0.05);
}
.content-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 768px) {
.content-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.content-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
}

View file

@ -1,6 +1,8 @@
/* Default cursor for all elements */ /* Default cursor for all elements */
* { * {
cursor: url('@/assets/cursors/default.svg') 16 16, auto; cursor:
url("@/assets/cursors/default.svg") 16 16,
auto;
} }
/* Interactive elements cursor */ /* Interactive elements cursor */
@ -12,14 +14,18 @@ input[type="button"],
select, select,
.interactive, .interactive,
.nav-link { .nav-link {
cursor: url('@/assets/cursors/paw.svg') 16 16, pointer; cursor:
url("@/assets/cursors/paw.svg") 16 16,
pointer;
} }
/* Loading state cursor */ /* Loading state cursor */
.loading, .loading,
:disabled, :disabled,
[aria-busy="true"] { [aria-busy="true"] {
cursor: url('@/assets/cursors/tail-loading.svg') 16 16, progress; cursor:
url("@/assets/cursors/tail-loading.svg") 16 16,
progress;
} }
/* Hover effects for interactive elements */ /* Hover effects for interactive elements */
@ -27,21 +33,21 @@ a:hover,
button:hover, button:hover,
[role="button"]:hover, [role="button"]:hover,
.nav-link:hover { .nav-link:hover {
filter: drop-shadow(0 0 4px var(--fox-pink-glow)); filter: drop-shadow(0 0 4px var(--fox-pink-glow));
transition: filter 0.3s ease; transition: filter 0.3s ease;
} }
/* Custom cursor regions */ /* Custom cursor regions */
.text-select { .text-select {
cursor: text; cursor: text;
} }
.resize { .resize {
cursor: nw-resize; cursor: nw-resize;
} }
/* Prevent cursor inheritance in certain cases */ /* Prevent cursor inheritance in certain cases */
iframe, iframe,
canvas { canvas {
cursor: inherit; cursor: inherit;
} }

View file

@ -1,26 +1,30 @@
/* Game-specific styles */ /* Game-specific styles */
.game-active * { .game-active * {
cursor: none; cursor: none;
} }
.game-viewport { .game-viewport {
touch-action: none; touch-action: none;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
} }
/* Animation utilities */ /* Animation utilities */
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
.fade-in { .fade-in {
animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out;
} }
/* Game-specific shadows */ /* Game-specific shadows */
.fox-shadow { .fox-shadow {
filter: drop-shadow(0 0 8px var(--fox-pink-glow)); filter: drop-shadow(0 0 8px var(--fox-pink-glow));
} }
.enemy-shadow { .enemy-shadow {
filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.3)); filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.3));
} }

View file

@ -1,173 +1,250 @@
/* src/styles/game.css */ /* src/styles/game.css */
@import 'base.css'; @import "base.css";
@import 'animations.css'; @import "animations.css";
@import 'utilities.css'; @import "utilities.css";
@import 'cursor.css'; @import "cursor.css";
/* Base game styles */ /* Base game styles */
.game-viewport { .game-viewport {
touch-action: none; touch-action: none;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
overflow: hidden; overflow: hidden;
background: linear-gradient( background: linear-gradient(
135deg, 135deg,
var(--background-primary) 0%, var(--background-primary) 0%,
var(--background-secondary) 100% var(--background-secondary) 100%
); );
} }
/* Game UI elements */ /* Game UI elements */
.game-hud { .game-hud {
pointer-events: none; pointer-events: none;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.game-card { .game-card {
background: rgba(47, 28, 84, 0.3); background: rgba(47, 28, 84, 0.3);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border: 1px solid rgba(157, 78, 221, 0.2); border: 1px solid rgba(157, 78, 221, 0.2);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.game-card:hover { .game-card:hover {
border-color: rgba(178, 73, 248, 0.4); border-color: rgba(178, 73, 248, 0.4);
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2); box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
} }
/* Player animations */ /* Player animations */
.player-idle { .player-idle {
animation: playerIdle 2s ease-in-out infinite; animation: playerIdle 2s ease-in-out infinite;
} }
.player-move { .player-move {
animation: playerMove 0.3s linear infinite; animation: playerMove 0.3s linear infinite;
} }
.player-hit { .player-hit {
animation: playerHit 0.5s ease-in-out; animation: playerHit 0.5s ease-in-out;
} }
@keyframes playerIdle { @keyframes playerIdle {
0%, 100% { transform: translate(-50%, -50%); } 0%,
50% { transform: translate(-50%, calc(-50% - 4px)); } 100% {
transform: translate(-50%, -50%);
}
50% {
transform: translate(-50%, calc(-50% - 4px));
}
} }
@keyframes playerMove { @keyframes playerMove {
0% { transform: rotate(-2deg); } 0% {
50% { transform: rotate(2deg); } transform: rotate(-2deg);
100% { transform: rotate(-2deg); } }
50% {
transform: rotate(2deg);
}
100% {
transform: rotate(-2deg);
}
} }
@keyframes playerHit { @keyframes playerHit {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.5; } 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
} }
/* Enemy animations */ /* Enemy animations */
.enemy-patrol { .enemy-patrol {
animation: enemyPatrol 3s linear infinite; animation: enemyPatrol 3s linear infinite;
} }
.enemy-chase { .enemy-chase {
animation: enemyChase 0.5s ease-in-out infinite; animation: enemyChase 0.5s ease-in-out infinite;
} }
@keyframes enemyPatrol { @keyframes enemyPatrol {
0% { transform: translateX(0); } 0% {
50% { transform: translateX(50px); } transform: translateX(0);
100% { transform: translateX(0); } }
50% {
transform: translateX(50px);
}
100% {
transform: translateX(0);
}
} }
@keyframes enemyChase { @keyframes enemyChase {
0%, 100% { transform: scale(1); } 0%,
50% { transform: scale(1.1); } 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
} }
/* Collectible animations */ /* Collectible animations */
.collectible { .collectible {
animation: collectibleFloat 2s ease-in-out infinite; animation: collectibleFloat 2s ease-in-out infinite;
} }
.collectible-gem { .collectible-gem {
animation: collectibleGem 3s linear infinite; animation: collectibleGem 3s linear infinite;
} }
@keyframes collectibleFloat { @keyframes collectibleFloat {
0%, 100% { transform: translateY(0); } 0%,
50% { transform: translateY(-10px); } 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
} }
@keyframes collectibleGem { @keyframes collectibleGem {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* Power-up effects */ /* Power-up effects */
.powerup-active { .powerup-active {
animation: powerupPulse 1s ease-in-out infinite; animation: powerupPulse 1s ease-in-out infinite;
} }
.powerup-shield { .powerup-shield {
animation: shieldRotate 3s linear infinite; animation: shieldRotate 3s linear infinite;
} }
@keyframes powerupPulse { @keyframes powerupPulse {
0%, 100% { filter: brightness(1); } 0%,
50% { filter: brightness(1.5); } 100% {
filter: brightness(1);
}
50% {
filter: brightness(1.5);
}
} }
@keyframes shieldRotate { @keyframes shieldRotate {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* Game effects */ /* Game effects */
.particle { .particle {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
animation: particleFade 1s ease-out forwards; animation: particleFade 1s ease-out forwards;
} }
@keyframes particleFade { @keyframes particleFade {
0% { transform: scale(1); opacity: 1; } 0% {
100% { transform: scale(0); opacity: 0; } transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0);
opacity: 0;
}
} }
/* Game UI animations */ /* Game UI animations */
.score-popup { .score-popup {
animation: scorePopup 0.5s ease-out forwards; animation: scorePopup 0.5s ease-out forwards;
} }
.health-change { .health-change {
animation: healthChange 0.5s ease-in-out; animation: healthChange 0.5s ease-in-out;
} }
@keyframes scorePopup { @keyframes scorePopup {
0% { transform: scale(0) translateY(0); opacity: 1; } 0% {
50% { transform: scale(1.2) translateY(-20px); opacity: 1; } transform: scale(0) translateY(0);
100% { transform: scale(1) translateY(-40px); opacity: 0; } opacity: 1;
}
50% {
transform: scale(1.2) translateY(-20px);
opacity: 1;
}
100% {
transform: scale(1) translateY(-40px);
opacity: 0;
}
} }
@keyframes healthChange { @keyframes healthChange {
0%, 100% { transform: scale(1); } 0%,
50% { transform: scale(1.2); } 100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
} }
/* Menu transitions */ /* Menu transitions */
.menu-enter { .menu-enter {
animation: menuEnter 0.3s ease-out forwards; animation: menuEnter 0.3s ease-out forwards;
} }
.menu-exit { .menu-exit {
animation: menuExit 0.3s ease-in forwards; animation: menuExit 0.3s ease-in forwards;
} }
@keyframes menuEnter { @keyframes menuEnter {
from { opacity: 0; transform: scale(0.9); } from {
to { opacity: 1; transform: scale(1); } opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
} }
@keyframes menuExit { @keyframes menuExit {
from { opacity: 1; transform: scale(1); } from {
to { opacity: 0; transform: scale(1.1); } opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(1.1);
}
} }
/* Custom cursor */ /* Custom cursor */
.game-cursor { .game-cursor {
width: 24px; width: 24px;
height: 24px; height: 24px;
pointer-events: none; pointer-events: none;
position: fixed; position: fixed;
z-index: 9999; z-index: 9999;
mix-blend-mode: difference; mix-blend-mode: difference;
transition: transform 0.1s ease; transition: transform 0.1s ease;
} }
/* Responsive adjustments */ /* Responsive adjustments */
@media (max-width: 640px) { @media (max-width: 640px) {
.game-hud { .game-hud {
font-size: 0.875rem; font-size: 0.875rem;
} }
.game-card { .game-card {
padding: 1rem; padding: 1rem;
} }
} }
/* Accessibility */ /* Accessibility */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.player-idle, .player-idle,
.player-move, .player-move,
.enemy-patrol, .enemy-patrol,
.collectible, .collectible,
.powerup-active { .powerup-active {
animation: none; animation: none;
} }
} }

View file

@ -1,65 +1,71 @@
/* Page Container */ /* Page Container */
.page-container { .page-container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
min-height: calc(100vh - 4rem); min-height: calc(100vh - 4rem);
} }
/* Header Card */ /* Header Card */
.header-card { .header-card {
margin-bottom: 2rem; margin-bottom: 2rem;
text-align: center; text-align: center;
background: var(--gradient-primary); background: var(--gradient-primary);
} }
.header-card h1 { .header-card h1 {
font-size: 2.5rem; font-size: 2.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
/* Content Grid */ /* Content Grid */
.content-grid { .content-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem; gap: 2rem;
} }
/* Interest List */ /* Interest List */
.interest-list { .interest-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 1rem 0; margin: 1rem 0;
} }
.interest-list li { .interest-list li {
padding: 0.5rem 0; padding: 0.5rem 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.interest-list li::before { .interest-list li::before {
content: "🦊"; content: "🦊";
margin-right: 0.5rem; margin-right: 0.5rem;
} }
/* Project Demo */ /* Project Demo */
.project-demo { .project-demo {
margin-top: 1rem; margin-top: 1rem;
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
overflow: hidden; overflow: hidden;
} }
/* Responsive Design */ /* Responsive Design */
@media (max-width: 768px) { @media (max-width: 768px) {
.page-container { .page-container {
padding: 1rem; padding: 1rem;
} }
.header-card h1 { .header-card h1 {
font-size: 2rem; font-size: 2rem;
} }
.content-grid { .content-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
/* Animation Classes */ /* Animation Classes */
.fade-in { .fade-in {
animation: fadeIn 0.5s ease-in; animation: fadeIn 0.5s ease-in;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }

View file

@ -1,26 +1,26 @@
body.protofox-theme {
/* Override core variables with protofox colors */
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #FF7E5F;
--accent-secondary: #00E5FF;
--text-glow: #4dffc7;
--visor-glow: #FF005C;
--fox-orange: #ff9466;
--fox-orange-dark: #e07242;
--proto-blue: #4dc3ff;
--proto-cyan: #00e5ff;
--proto-neon: #39FFBF;
--circuit-line: rgba(0, 229, 255, 0.2);
--circuit-node: rgba(0, 229, 255, 0.4);
/* Apply protofox font overrides */
--heading-font: 'Orbitron', sans-serif;
--body-font: 'Exo 2', sans-serif;
}
/* Font imports and assignments */ /* Font imports and assignments */
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap'); @import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap");
body.protofox-theme {
/* Override core variables with protofox colors */
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #ff7e5f;
--accent-secondary: #00e5ff;
--text-glow: #4dffc7;
--visor-glow: #ff005c;
--fox-orange: #ff9466;
--fox-orange-dark: #e07242;
--proto-blue: #4dc3ff;
--proto-cyan: #00e5ff;
--proto-neon: #39ffbf;
--circuit-line: rgba(0, 229, 255, 0.2);
--circuit-node: rgba(0, 229, 255, 0.4);
/* Apply protofox font overrides */
--heading-font: "Orbitron", sans-serif;
--body-font: "Exo 2", sans-serif;
}
body.protofox-theme h1, body.protofox-theme h1,
body.protofox-theme h2, body.protofox-theme h2,
@ -28,198 +28,209 @@ body.protofox-theme h3,
body.protofox-theme h4, body.protofox-theme h4,
body.protofox-theme h5, body.protofox-theme h5,
body.protofox-theme h6 { body.protofox-theme h6 {
font-family: var(--heading-font); font-family: var(--heading-font);
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
body.protofox-theme { body.protofox-theme {
font-family: var(--body-font); font-family: var(--body-font);
} }
/* Add circuit background only in protofox mode */ /* Add circuit background only in protofox mode */
body.protofox-theme::before { body.protofox-theme::before {
content: ''; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
background-color: var(--background-primary); background-color: var(--background-primary);
background-image: background-image:
radial-gradient(var(--circuit-node) 2px, transparent 2px), radial-gradient(var(--circuit-node) 2px, transparent 2px),
linear-gradient(to right, var(--circuit-line) 1px, transparent 1px), linear-gradient(to right, var(--circuit-line) 1px, transparent 1px),
linear-gradient(to bottom, var(--circuit-line) 1px, transparent 1px); linear-gradient(to bottom, var(--circuit-line) 1px, transparent 1px);
background-size: 30px 30px, 30px 30px, 30px 30px; background-size:
background-position: 0 0; 30px 30px,
opacity: 0.15; 30px 30px,
z-index: -1; 30px 30px;
pointer-events: none; background-position: 0 0;
opacity: 0.15;
z-index: -1;
pointer-events: none;
} }
/* ProtoFox mode cursor */ /* ProtoFox mode cursor */
body.protofox-theme { body.protofox-theme {
cursor: url('/cursors/protofox-default.svg') 16 16, auto; cursor:
url("/cursors/protofox-default.svg") 16 16,
auto;
} }
body.protofox-theme a, body.protofox-theme a,
body.protofox-theme button, body.protofox-theme button,
body.protofox-theme [role="button"], body.protofox-theme [role="button"],
body.protofox-theme .interactive { body.protofox-theme .interactive {
cursor: url('/cursors/protofox-pointer.svg') 16 16, pointer; cursor:
url("/cursors/protofox-pointer.svg") 16 16,
pointer;
} }
/* Add scan line animation */ /* Add scan line animation */
body.protofox-theme::after { body.protofox-theme::after {
content: ''; content: "";
position: fixed; position: fixed;
top: -10px; top: -10px;
left: 0; left: 0;
width: 100%; width: 100%;
height: 2px; height: 2px;
background: var(--accent-secondary); background: var(--accent-secondary);
opacity: 0.03; opacity: 0.03;
box-shadow: 0 0 8px var(--accent-secondary); box-shadow: 0 0 8px var(--accent-secondary);
animation: protofox-scan 8s linear infinite; animation: protofox-scan 8s linear infinite;
pointer-events: none; pointer-events: none;
z-index: 9999; z-index: 9999;
} }
@keyframes protofox-scan { @keyframes protofox-scan {
0% { top: -10px; } 0% {
100% { top: 100vh; } top: -10px;
}
100% {
top: 100vh;
}
} }
/* Enhance existing components in protofox mode */ /* Enhance existing components in protofox mode */
/* Cards */ /* Cards */
body.protofox-theme .fox-card { body.protofox-theme .fox-card {
border-color: rgba(0, 229, 255, 0.2); border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
body.protofox-theme .fox-card::before { body.protofox-theme .fox-card::before {
content: ''; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background: url('/images/protofox/fur-texture.png'); background: url("/images/protofox/fur-texture.png");
opacity: 0.04; opacity: 0.04;
mix-blend-mode: overlay; mix-blend-mode: overlay;
pointer-events: none; pointer-events: none;
} }
body.protofox-theme .fox-card::after { body.protofox-theme .fox-card::after {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--proto-cyan); background: var(--proto-cyan);
opacity: 0.5; opacity: 0.5;
box-shadow: 0 0 5px var(--proto-cyan); box-shadow: 0 0 5px var(--proto-cyan);
} }
/* Buttons */ /* Buttons */
body.protofox-theme button:not(.theme-toggle *), body.protofox-theme button:not(.theme-toggle *),
body.protofox-theme .button { body.protofox-theme .button {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
body.protofox-theme button:not(.theme-toggle *):before, body.protofox-theme button:not(.theme-toggle *):before,
body.protofox-theme .button:before { body.protofox-theme .button:before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--proto-cyan); background: var(--proto-cyan);
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
body.protofox-theme button:hover:before, body.protofox-theme button:hover:before,
body.protofox-theme .button:hover:before { body.protofox-theme .button:hover:before {
opacity: 1; opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan); box-shadow: 0 0 5px var(--proto-cyan);
} }
/* Navigation */ /* Navigation */
body.protofox-theme .navbar { body.protofox-theme .navbar {
position: relative; position: relative;
} }
body.protofox-theme .navbar::before { body.protofox-theme .navbar::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--proto-cyan); background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan); box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1; z-index: 1;
} }
body.protofox-theme .nav-link { body.protofox-theme .nav-link {
position: relative; position: relative;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
body.protofox-theme .nav-link::after { body.protofox-theme .nav-link::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -2px; bottom: -2px;
left: 0; left: 0;
width: 0; width: 0;
height: 2px; height: 2px;
background: var(--accent-primary); background: var(--accent-primary);
box-shadow: 0 0 8px var(--accent-primary); box-shadow: 0 0 8px var(--accent-primary);
transition: width 0.3s ease; transition: width 0.3s ease;
} }
body.protofox-theme .nav-link:hover::after, body.protofox-theme .nav-link:hover::after,
body.protofox-theme .nav-link.active::after { body.protofox-theme .nav-link.active::after {
width: 100%; width: 100%;
} }
/* WebRing Enhancements */ /* WebRing Enhancements */
body.protofox-theme .webring-button { body.protofox-theme .webring-button {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
body.protofox-theme .webring-button::before { body.protofox-theme .webring-button::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--proto-cyan); background: var(--proto-cyan);
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
body.protofox-theme .webring-button:hover::before { body.protofox-theme .webring-button:hover::before {
opacity: 1; opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan); box-shadow: 0 0 5px var(--proto-cyan);
} }
/* More Foxxos Button Enhancements */ /* More Foxxos Button Enhancements */
body.protofox-theme .foxxos-container { body.protofox-theme .foxxos-container {
border-color: rgba(0, 229, 255, 0.2); border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
} }
body.protofox-theme .foxxos-container::before { body.protofox-theme .foxxos-container::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: var(--proto-cyan); background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan); box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1; z-index: 1;
} }

View file

@ -1,20 +1,32 @@
/* Base theme colors */ /* Base theme colors */
:root { :root {
/* Main colors */ /* Main colors */
--background-primary: #1a0b2e; --background-primary: #1a0b2e;
--background-secondary: #2f1c54; --background-secondary: #2f1c54;
--accent-primary: #9d4edd; --accent-primary: #9d4edd;
--accent-neon: #b249f8; --accent-neon: #b249f8;
--text-glow: #e0aaff; --text-glow: #e0aaff;
--text-primary: #ffffff; --text-primary: #ffffff;
--dark-accent: #240046; --dark-accent: #240046;
/* Fox theme accents */ /* Fox theme accents */
--fox-pink: #ffc6e5; --fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6; --fox-pink-glow: #ffadd6;
--fox-orange: #ff9466; --fox-orange: #ff9466;
--fox-white: #fff5f9; --fox-white: #fff5f9;
/* Gradients */ /* Gradients */
--gradient-primary: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%); --gradient-primary: linear-gradient(
--gradient-card: linear-gradient(135deg, rgba(47, 28, 84, 0.3) 0%, rgba(157, 78, 221, 0.1) 100%); 135deg,
--gradient-hover: linear-gradient(135deg, rgba(157, 78, 221, 0.2) 0%, rgba(178, 73, 248, 0.1) 100%); var(--background-primary) 0%,
var(--background-secondary) 100%
);
--gradient-card: linear-gradient(
135deg,
rgba(47, 28, 84, 0.3) 0%,
rgba(157, 78, 221, 0.1) 100%
);
--gradient-hover: linear-gradient(
135deg,
rgba(157, 78, 221, 0.2) 0%,
rgba(178, 73, 248, 0.1) 100%
);
} }

View file

@ -1,78 +1,79 @@
@layer utilities { @layer utilities {
/* Animated background */ /* Animated background */
.animated-bg { .animated-bg {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.animated-bg::before { .animated-bg::before {
content: ''; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
opacity: 0.5; opacity: 0.5;
background: var(--gradient-primary); background: var(--gradient-primary);
background-size: 200% 200%; background-size: 200% 200%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
} }
/* Fox ear styling */ /* Fox ear styling */
.fox-ear { .fox-ear {
position: absolute; position: absolute;
width: 30px; width: 30px;
height: 30px; height: 30px;
background-color: var(--fox-pink); background-color: var(--fox-pink);
opacity: 0.1; opacity: 0.1;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
.fox-ear-left { .fox-ear-left {
top: -15px; top: -15px;
left: -15px; left: -15px;
transform: rotate(45deg); transform: rotate(45deg);
border-radius: 0 0 0 15px; border-radius: 0 0 0 15px;
} }
.fox-ear-right { .fox-ear-right {
top: -15px; top: -15px;
right: -15px; right: -15px;
transform: rotate(-45deg); transform: rotate(-45deg);
border-radius: 0 0 15px 0; border-radius: 0 0 15px 0;
} }
/* Hover effects */ /* Hover effects */
.hover-glow { .hover-glow {
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.hover-glow:hover { .hover-glow:hover {
filter: drop-shadow(0 0 4px var(--fox-pink-glow)); filter: drop-shadow(0 0 4px var(--fox-pink-glow));
} }
/* Spacing utilities */ /* Spacing utilities */
.section-spacing { .section-spacing {
margin-top: 2rem; margin-top: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.section-spacing > * + * { .section-spacing > * + * {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.content-spacing { .content-spacing {
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.content-spacing > * + * { .content-spacing > * + * {
margin-top: 1rem; margin-top: 1rem;
} }
} }
@keyframes gradientShift { @keyframes gradientShift {
0%, 100% { 0%,
background-position: 0% 50%; 100% {
} background-position: 0% 50%;
50% { }
background-position: 100% 50%; 50% {
} background-position: 100% 50%;
} }
}

View file

@ -1,57 +1,57 @@
export interface Position { export interface Position {
x: number; x: number;
y: number; y: number;
} }
export interface PowerUp { export interface PowerUp {
id: string; id: string;
type: 'SPEED' | 'SHIELD' | 'MAGNET'; type: "SPEED" | "SHIELD" | "MAGNET";
duration: number; duration: number;
position: Position; position: Position;
} }
export interface Collectible { export interface Collectible {
id: string; id: string;
type: 'STAR' | 'GEM' | 'KEY'; type: "STAR" | "GEM" | "KEY";
value: number; value: number;
position: Position; position: Position;
} }
export interface Enemy { export interface Enemy {
id: string; id: string;
type: 'WOLF' | 'OWL' | 'HUNTER'; type: "WOLF" | "OWL" | "HUNTER";
position: Position; position: Position;
direction: Position; direction: Position;
speed: number; speed: number;
} }
export interface PlayerState { export interface PlayerState {
position: Position; position: Position;
health: number; health: number;
speed: number; speed: number;
powerUps: PowerUp[]; powerUps: PowerUp[];
isInvincible: boolean; isInvincible: boolean;
hasKey: boolean; hasKey: boolean;
} }
export interface GameState { export interface GameState {
player: PlayerState; player: PlayerState;
enemies: Enemy[]; enemies: Enemy[];
collectibles: Collectible[]; collectibles: Collectible[];
powerUps: PowerUp[]; powerUps: PowerUp[];
score: number; score: number;
level: number; level: number;
gameStatus: 'MENU' | 'PLAYING' | 'PAUSED' | 'GAME_OVER'; gameStatus: "MENU" | "PLAYING" | "PAUSED" | "GAME_OVER";
highScores: number[]; highScores: number[];
timePlayed: number; timePlayed: number;
// Actions // Actions
movePlayer: (direction: Position) => void; movePlayer: (direction: Position) => void;
updateEnemies: (deltaTime?: number) => void; updateEnemies: (deltaTime?: number) => void;
collectItem: (itemId: string) => void; collectItem: (itemId: string) => void;
takeDamage: (amount: number) => void; takeDamage: (amount: number) => void;
activatePowerUp: (powerUpId: string) => void; activatePowerUp: (powerUpId: string) => void;
startNewGame: () => void; startNewGame: () => void;
pauseGame: () => void; pauseGame: () => void;
resumeGame: () => void; resumeGame: () => void;
} }

View file

@ -1,4 +1,4 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
export interface Track { export interface Track {
id: string; id: string;

View file

@ -3,4 +3,4 @@ export function calculatePreciseAge(birthDate: Date): number {
const diffTime = today.getTime() - birthDate.getTime(); const diffTime = today.getTime() - birthDate.getTime();
const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25); const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25);
return Number(diffYears.toFixed(8)); return Number(diffYears.toFixed(8));
} }

View file

@ -1,32 +1,39 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import react from '@vitejs/plugin-react-swc' import react from "@vitejs/plugin-react-swc";
import path from 'path' import babel from 'vite-plugin-babel';
import path from "path";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [
resolve: { react(),
alias: { babel({
'@': path.resolve(__dirname, './src'), babelConfig: {
}, plugins: ['babel-plugin-react-compiler'],
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8787',
changeOrigin: true,
}, },
}, }),],
}, resolve: {
build: { alias: {
outDir: 'dist', "@": path.resolve(__dirname, "./src"),
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
}, },
},
}, },
}, server: {
}) port: 3000,
proxy: {
"/api": {
target: "http://localhost:8787",
changeOrigin: true,
},
},
},
build: {
outDir: "dist",
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
"react-vendor": ["react", "react-dom", "react-router-dom"],
},
},
},
},
});