Formating and small updates

This commit is contained in:
End 2025-09-19 08:52:17 -07:00
parent 48c78896a5
commit d1a66fc794
No known key found for this signature in database
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
### 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

View file

@ -1,28 +1,39 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<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>
<head>
<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 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",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"eslint-config-react-app": "7.0.1",
"itty-router": "^4.0.27",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -39,14 +40,16 @@
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "^1.2.2",
"autoprefixer": "10.4.20",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^8.56.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",
"husky": "^9.0.10",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"lucide-react": "0.474.0",
"plugin-react": "0.0.1-security",
"postcss": "8.5.1",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
@ -54,6 +57,7 @@
"tailwindcss": "3.4.17",
"typescript": "^5.3.3",
"vite": "6.2.2",
"vite-plugin-babel": "1.3.2",
"vitest": "3.0.9",
"web-vitals": "4.2.4",
"wrangler": "^3.28.0"

View file

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

View file

@ -1,215 +1,269 @@
import React, { useState, useEffect } from 'react';
import '@/styles/EndOSBootAnimation.css';
import React, { useState, useEffect } from "react";
import "@/styles/EndOSBootAnimation.css";
interface EndOSBootAnimationProps {
// Using underscores to mark unused props to avoid TypeScript errors
_customLogo?: string;
_customColors?: {
primary?: string;
secondary?: string;
fox?: string;
};
onComplete?: () => void;
skipAnimation?: boolean;
// Using underscores to mark unused props to avoid TypeScript errors
_customLogo?: string;
_customColors?: {
primary?: string;
secondary?: string;
fox?: string;
};
onComplete?: () => void;
skipAnimation?: boolean;
}
const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
onComplete,
skipAnimation = false,
// Rename with underscores to indicate variables are intentionally unused
_customLogo,
_customColors
const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
onComplete,
skipAnimation = false,
// Rename with underscores to indicate variables are intentionally unused
_customLogo,
_customColors,
}) => {
const [active, setActive] = useState(true);
const [bootStage, setBootStage] = useState(0);
const [showLogo, setShowLogo] = useState(false);
const [bootComplete, setBootComplete] = useState(false);
const [active, setActive] = useState(true);
const [bootStage, setBootStage] = useState(0);
const [showLogo, setShowLogo] = useState(false);
const [bootComplete, setBootComplete] = useState(false);
// Boot sequence timing
useEffect(() => {
if (skipAnimation) {
handleAnimationComplete();
return;
}
// Initialize boot sequence with proper timing to prevent overlap
const bootSequence = [
{ stage: 1, delay: 1500 }, // Initial screen flicker
{ stage: 2, delay: 3000 }, // BIOS check (longer time to read)
{ stage: 3, delay: 3500 }, // System scan (longer for progress bar)
{ stage: 4, delay: 3500 }, // Loading modules (allow time for animation)
{ stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits)
{ stage: 6, delay: 3000 }, // Show logo (allow time to appreciate)
{ stage: 7, delay: 3000 }, // Final activation (longer read time)
{ stage: 8, delay: 2000 } // Fade out
];
let timeout: any; // Using any instead of NodeJS.Timeout
let currentIndex = 0;
const runNextStage = () => {
if (currentIndex < bootSequence.length) {
const { stage, delay } = bootSequence[currentIndex];
// Clean transition - clear ALL previous stages to prevent any background visibility
document.querySelectorAll('.boot-stage').forEach(el => {
el.classList.remove('active');
});
// Reset content visibility
document.querySelectorAll('.boot-content').forEach(el => {
el.classList.remove('active');
});
// Short delay to allow for transition
setTimeout(() => {
// First ensure boot content is visible
document.querySelectorAll('.boot-content').forEach(el => {
el.classList.add('active');
});
// Then activate the correct stage
setBootStage(stage);
if (stage === 6) {
setShowLogo(true);
} else if (stage === 7) {
setBootComplete(true);
}
}, 300);
currentIndex++;
timeout = setTimeout(runNextStage, delay);
} else {
// Boot sequence timing
useEffect(() => {
if (skipAnimation) {
handleAnimationComplete();
return;
}
// Initialize boot sequence with proper timing to prevent overlap
const bootSequence = [
{ stage: 1, delay: 1500 }, // Initial screen flicker
{ stage: 2, delay: 3000 }, // BIOS check (longer time to read)
{ stage: 3, delay: 3500 }, // System scan (longer for progress bar)
{ stage: 4, delay: 3500 }, // Loading modules (allow time for animation)
{ stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits)
{ stage: 6, delay: 3000 }, // Show logo (allow time to appreciate)
{ stage: 7, delay: 3000 }, // Final activation (longer read time)
{ stage: 8, delay: 2000 }, // Fade out
];
let timeout: any; // Using any instead of NodeJS.Timeout
let currentIndex = 0;
const runNextStage = () => {
if (currentIndex < bootSequence.length) {
const { stage, delay } = bootSequence[currentIndex];
// Clean transition - clear ALL previous stages to prevent any background visibility
document.querySelectorAll(".boot-stage").forEach((el) => {
el.classList.remove("active");
});
// Reset content visibility
document.querySelectorAll(".boot-content").forEach((el) => {
el.classList.remove("active");
});
// Short delay to allow for transition
setTimeout(() => {
// First ensure boot content is visible
document.querySelectorAll(".boot-content").forEach((el) => {
el.classList.add("active");
});
// Then activate the correct stage
setBootStage(stage);
if (stage === 6) {
setShowLogo(true);
} else if (stage === 7) {
setBootComplete(true);
}
}, 300);
currentIndex++;
timeout = setTimeout(runNextStage, delay);
} 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();
}
};
// 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();
};
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>
if (!active) return null;
return (
<div className="endos-boot-container">
{/* Accessibility */}
<div className="visually-hidden">
Website loading. EndOS boot sequence in progress.
</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 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>
{/* 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 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>
{/* Skip button - More prominent and always visible */}
<button
className="skip-button"
onClick={handleSkip}
aria-label="Skip boot animation"
>
SKIP BOOT SEQUENCE
</button>
</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 { ErrorBoundaryProps, ErrorBoundaryState } from '@/types';
import { Component, ErrorInfo } from "react";
import { ErrorBoundaryProps, ErrorBoundaryState } from "@/types";
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
@ -12,7 +12,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error('Error caught by boundary:', error, errorInfo);
console.error("Error caught by boundary:", error, errorInfo);
}
render() {

View file

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

View file

@ -1,5 +1,5 @@
import { GithubRepo } from '@/types';
import '@/styles/GithubRepos.css';
import { GithubRepo } from "@/types";
import "@/styles/GithubRepos.css";
interface GithubReposProps {
repos: GithubRepo[];
@ -11,14 +11,21 @@ const GithubRepos: React.FC<GithubReposProps> = ({ repos }) => {
<div className="repos-grid">
{repos.map((repo) => (
<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}
</a>
<p className="repo-description">
{repo.description || 'No description provided.'}
{repo.description || "No description provided."}
</p>
{repo.language && (
<span className="repo-language">{repo.language}</span>
<span className="repo-language">
{repo.language}
</span>
)}
{repo.languages && repo.languages.length > 0 && (
<div className="repo-languages">

View file

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

View file

@ -1,161 +1,173 @@
import { useState, useEffect } from 'react';
import { Music } from 'lucide-react';
import { useState, useEffect } from "react";
import { Music } from "lucide-react";
interface LastFMImage {
'#text': string;
size: string;
"#text": string;
size: string;
}
interface LastFMTrack {
name: string;
artist: {
'#text': string;
};
image: LastFMImage[];
'@attr'?: {
nowplaying: string;
};
name: string;
artist: {
"#text": string;
};
image: LastFMImage[];
"@attr"?: {
nowplaying: string;
};
}
interface LastFMResponse {
recenttracks: {
track: LastFMTrack[];
};
recenttracks: {
track: LastFMTrack[];
};
}
interface CurrentTrack {
name: string;
artist: string;
image: string;
isPlaying: boolean;
name: string;
artist: string;
image: string;
isPlaying: boolean;
}
const MusicDisplay = () => {
const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false);
const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const fetchCurrentTrack = async () => {
try {
const API_KEY = import.meta.env.VITE_LASTFM_API_KEY;
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
useEffect(() => {
const fetchCurrentTrack = async () => {
try {
const API_KEY = import.meta.env.VITE_LASTFM_API_KEY;
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
if (!API_KEY || !USERNAME) {
throw new Error('Last.fm API key or username not configured');
}
if (!API_KEY || !USERNAME) {
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 response = await fetch(url);
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);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: LastFMResponse = await response.json();
if (!data.recenttracks?.track?.length) {
setCurrentTrack(null);
setExpanded(true);
return;
}
const data: LastFMResponse = await response.json();
const track = data.recenttracks.track[0];
const largeImage = track.image.find(img => img.size === 'large');
const imageUrl = largeImage ? largeImage['#text'] :
track.image[track.image.length - 1] ? track.image[track.image.length - 1]['#text'] :
'/placeholder-album.jpg';
if (!data.recenttracks?.track?.length) {
setCurrentTrack(null);
setExpanded(true);
return;
}
const isCurrentlyPlaying = track['@attr']?.nowplaying === 'true';
const track = data.recenttracks.track[0];
if (!isCurrentlyPlaying) {
setCurrentTrack(null);
setExpanded(true);
return;
}
const largeImage = track.image.find(
(img) => img.size === "large",
);
const imageUrl = largeImage
? largeImage["#text"]
: track.image[track.image.length - 1]
? track.image[track.image.length - 1]["#text"]
: "/placeholder-album.jpg";
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);
}
};
const isCurrentlyPlaying =
track["@attr"]?.nowplaying === "true";
fetchCurrentTrack();
const interval = setInterval(fetchCurrentTrack, 30000);
return () => clearInterval(interval);
}, []);
if (!isCurrentlyPlaying) {
setCurrentTrack(null);
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 (
<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>
);
}
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 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 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>
</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 { Home, Code, BookOpen, Twitch} from 'lucide-react';
import { Link, useLocation } from "react-router-dom";
import { Home, Code, BookOpen, Twitch } from "lucide-react";
const Navbar = () => {
const location = useLocation();
const handleRedirect = () => {
window.open("https://vnc.endoftimee.tech", "_blank");
};
return (
<nav className="navbar">
<div className="nav-content">
<div className="nav-links">
<Link
to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
<Link
to="/"
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
>
<Home size={20} />
<span>About</span>
</Link>
<Link
to="/projects"
className={`nav-link ${location.pathname === '/projects' ? 'active' : ''}`}
<Link
to="/projects"
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
>
<Code size={20} />
<span>Projects</span>
</Link>
{/*
<Link
to="/apcsp"
className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`}
>
<BookOpen size={20} />
<span>APCSP</span>
</Link>
</Link> */}
<Link
to="/novnc"
className={`nav-link ${location.pathname === '/novnc' ? 'active' : ''}`}
<button
onClick={handleRedirect}
className="nav-link"
style={{
background: "none",
border: "none",
cursor: "pointer",
padding: "0 15px",
display: "flex",
alignItems: "center",
}}
>
<Code size={20} />
<span>noVNC</span>
</Link>
<span>NoVNC</span>
</button>
<a
<a
href="https://twitch.tv/EndofTimee"
target="_blank"
rel="noopener noreferrer"
@ -49,14 +60,14 @@ const Navbar = () => {
<Twitch size={20} />
<span>Stream</span>
</a>
<div className="nav-link">
<iframe
src="https://github.com/sponsors/EndofTimee/button"
title="Sponsor EndofTimee"
height="32"
width="114"
style={{ border: 0, borderRadius: '6px' }}
<iframe
src="https://github.com/sponsors/System-End/button"
title="Sponsor End!"
height="32"
width="114"
style={{ border: 0, borderRadius: "6px" }}
></iframe>
</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 { ReactNode } from 'react';

View file

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

View file

@ -1,109 +1,109 @@
.theme-toggle-container {
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 0.5rem;
border-radius: 0.5rem;
backdrop-filter: blur(5px);
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
background-color: rgba(0, 0, 0, 0.3);
padding: 0.5rem;
border-radius: 0.5rem;
backdrop-filter: blur(5px);
}
.theme-toggle {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
}
.theme-toggle input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.toggle-track {
width: 60px;
height: 30px;
background-color: var(--background-secondary);
border-radius: 15px;
position: relative;
transition: all 0.3s ease;
width: 60px;
height: 30px;
background-color: var(--background-secondary);
border-radius: 15px;
position: relative;
transition: all 0.3s ease;
}
.toggle-indicator {
position: absolute;
width: 26px;
height: 26px;
background-color: var(--text-primary);
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
overflow: visible;
position: absolute;
width: 26px;
height: 26px;
background-color: var(--text-primary);
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
overflow: visible;
}
/* Fox ears */
.fox-ear {
position: absolute;
width: 10px;
height: 10px;
background-color: var(--fox-orange);
top: -5px;
opacity: 0;
transition: all 0.3s ease;
position: absolute;
width: 10px;
height: 10px;
background-color: var(--fox-orange);
top: -5px;
opacity: 0;
transition: all 0.3s ease;
}
.fox-ear.left {
left: 3px;
transform: rotate(45deg);
border-radius: 0 0 0 5px;
left: 3px;
transform: rotate(45deg);
border-radius: 0 0 0 5px;
}
.fox-ear.right {
right: 3px;
transform: rotate(-45deg);
border-radius: 0 0 5px 0;
right: 3px;
transform: rotate(-45deg);
border-radius: 0 0 5px 0;
}
.fox-ear.active {
opacity: 1;
opacity: 1;
}
/* Visor element */
.toggle-visor {
position: absolute;
width: 16px;
height: 4px;
background-color: transparent;
top: 10px;
left: 5px;
border-radius: 2px;
transition: all 0.3s ease;
position: absolute;
width: 16px;
height: 4px;
background-color: transparent;
top: 10px;
left: 5px;
border-radius: 2px;
transition: all 0.3s ease;
}
/* When toggled to protofox mode */
input:checked + .toggle-track {
background-color: var(--background-primary);
background-color: var(--background-primary);
}
input:checked + .toggle-track .toggle-indicator {
left: 32px;
background-color: #121212;
left: 32px;
background-color: #121212;
}
input:checked + .toggle-track .toggle-visor {
background-color: #00E5FF;
box-shadow: 0 0 5px #00E5FF;
background-color: #00e5ff;
box-shadow: 0 0 5px #00e5ff;
}
.toggle-label {
margin-left: 10px;
font-size: 14px;
transition: all 0.3s ease;
margin-left: 10px;
font-size: 14px;
transition: all 0.3s ease;
}
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 './ThemeToggle.css';
import React, { useState, useEffect } from "react";
import "./ThemeToggle.css";
interface ThemeToggleProps {
className?: string;
className?: string;
}
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => {
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
// Check if user had previously selected protofox mode
return localStorage.getItem('theme') === 'protofox';
});
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = "" }) => {
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
// Check if user had previously selected protofox mode
return localStorage.getItem("theme") === "protofox";
});
// Apply theme class to body when toggle changes
useEffect(() => {
if (isProtoFoxMode) {
document.body.classList.add('protofox-theme');
localStorage.setItem('theme', 'protofox');
} else {
document.body.classList.remove('protofox-theme');
localStorage.setItem('theme', 'default');
}
}, [isProtoFoxMode]);
// Apply theme class to body when toggle changes
useEffect(() => {
if (isProtoFoxMode) {
document.body.classList.add("protofox-theme");
localStorage.setItem("theme", "protofox");
} else {
document.body.classList.remove("protofox-theme");
localStorage.setItem("theme", "default");
}
}, [isProtoFoxMode]);
return (
<div className={`theme-toggle-container ${className}`}>
<label className="theme-toggle">
<input
type="checkbox"
checked={isProtoFoxMode}
onChange={() => setIsProtoFoxMode(!isProtoFoxMode)}
/>
<div className="toggle-track">
<div className="toggle-indicator">
<div className={`fox-ear left ${isProtoFoxMode ? 'active' : ''}`}></div>
<div className={`fox-ear right ${isProtoFoxMode ? 'active' : ''}`}></div>
<div className="toggle-visor"></div>
</div>
return (
<div className={`theme-toggle-container ${className}`}>
<label className="theme-toggle">
<input
type="checkbox"
checked={isProtoFoxMode}
onChange={() => setIsProtoFoxMode(!isProtoFoxMode)}
/>
<div className="toggle-track">
<div className="toggle-indicator">
<div
className={`fox-ear left ${isProtoFoxMode ? "active" : ""}`}
></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>
<span className="toggle-label">
{isProtoFoxMode ? 'ProtoFox Mode' : 'Standard Mode'}
</span>
</label>
</div>
);
);
};
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 }) => (
<div className={`card ${className || ''}`}>{children}</div>
<div className={`card ${className || ""}`}>{children}</div>
);
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 }) => (
<h2 className={`card-title ${className || ''}`}>{children}</h2>
<h2 className={`card-title ${className || ""}`}>{children}</h2>
);
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 {
id: string;
name: string;
role: string;
color?: string;
id: string;
name: string;
role: string;
color?: string;
}
interface SystemState {
safetyLevel: 'safe' | 'unsafe' | 'sorta-safe' | 'unknown';
mentalState: 'ok' | 'bad' | 'very-bad' | 'panic' | 'spiraling' | 'unstable' | 'delusional';
frontingStatus: 'single' | 'co-fronting' | 'switching' | 'unknown';
currentFronters: SystemMember[];
safetyLevel: "safe" | "unsafe" | "sorta-safe" | "unknown";
mentalState:
| "ok"
| "bad"
| "very-bad"
| "panic"
| "spiraling"
| "unstable"
| "delusional";
frontingStatus: "single" | "co-fronting" | "switching" | "unknown";
currentFronters: SystemMember[];
}
interface AuthContextType {
isAuthenticated: boolean;
username: string | null;
systemState: SystemState | null;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
updateSystemState: (newState: Partial<SystemState>) => void;
isAuthenticated: boolean;
username: string | null;
systemState: SystemState | null;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
updateSystemState: (newState: Partial<SystemState>) => void;
}
// System members data
const systemMembers: SystemMember[] = [
{ id: '1', name: 'Aurora', role: 'Host', color: '#9d4edd' },
{ id: '2', name: 'Alex', role: 'Younger', color: '#4ea8de' },
{ id: '3', name: 'Psy', role: 'Protector', color: '#5e548e' },
{ id: '4', name: 'Xander', role: 'Caretaker', color: '#219ebc' },
{ id: '5', name: 'Unknown', role: 'Fragment', color: '#6c757d' },
{ id: '6', name: 'The thing', role: 'Persecutor', color: '#e63946' },
{ id: '7', name: 'Unknown 2', role: 'Fragment', color: '#6c757d' },
{ id: "1", name: "Aurora", role: "Host", color: "#9d4edd" },
{ id: "2", name: "Alex", role: "Younger", color: "#4ea8de" },
{ id: "3", name: "Psy", role: "Protector", color: "#5e548e" },
{ id: "4", name: "Xander", role: "Caretaker", color: "#219ebc" },
{ id: "5", name: "Unknown", role: "Fragment", color: "#6c757d" },
{ id: "6", name: "The thing", role: "Persecutor", color: "#e63946" },
{ id: "7", name: "Unknown 2", role: "Fragment", color: "#6c757d" },
];
// Creating the context with a default value of null
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
// Initialize authentication state from localStorage if available
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
const stored = localStorage.getItem('isAuthenticated');
return stored === 'true';
});
// Initialize authentication state from localStorage if available
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
const stored = localStorage.getItem("isAuthenticated");
return stored === "true";
});
const [username, setUsername] = useState<string | null>(() => {
return localStorage.getItem('username');
});
const [username, setUsername] = useState<string | null>(() => {
return localStorage.getItem("username");
});
// Initialize system state from localStorage or set defaults
const [systemState, setSystemState] = useState<SystemState | null>(() => {
const stored = localStorage.getItem('systemState');
if (stored && isAuthenticated) {
return JSON.parse(stored);
}
return isAuthenticated ? {
safetyLevel: 'safe',
mentalState: 'ok',
frontingStatus: 'single',
currentFronters: [systemMembers[0]] // Default to Aurora as fronter
} : null;
});
// Initialize system state from localStorage or set defaults
const [systemState, setSystemState] = useState<SystemState | null>(() => {
const stored = localStorage.getItem("systemState");
if (stored && isAuthenticated) {
return JSON.parse(stored);
}
return isAuthenticated
? {
safetyLevel: "safe",
mentalState: "ok",
frontingStatus: "single",
currentFronters: [systemMembers[0]], // Default to Aurora as fronter
}
: null;
});
// Update localStorage when auth state changes
useEffect(() => {
localStorage.setItem('isAuthenticated', isAuthenticated.toString());
if (username) {
localStorage.setItem('username', username);
} else {
localStorage.removeItem('username');
}
// Update localStorage when auth state changes
useEffect(() => {
localStorage.setItem("isAuthenticated", isAuthenticated.toString());
if (username) {
localStorage.setItem("username", username);
} else {
localStorage.removeItem("username");
}
// If logged out, clear system state
if (!isAuthenticated) {
localStorage.removeItem('systemState');
setSystemState(null);
} else if (systemState) {
localStorage.setItem('systemState', JSON.stringify(systemState));
}
}, [isAuthenticated, username, systemState]);
// If logged out, clear system state
if (!isAuthenticated) {
localStorage.removeItem("systemState");
setSystemState(null);
} else if (systemState) {
localStorage.setItem("systemState", JSON.stringify(systemState));
}
}, [isAuthenticated, username, systemState]);
const login = async (username: string, password: string) => {
// For security, add a slight delay to prevent rapid brute force attempts
await new Promise(resolve => setTimeout(resolve, 800));
const login = async (username: string, password: string) => {
// For security, add a slight delay to prevent rapid brute force attempts
await new Promise((resolve) => setTimeout(resolve, 800));
// We use credential verification with multiple allowed passwords for different contexts
const validCredentials = [
{ user: 'system', pass: '.' },
];
const isValid = validCredentials.some(
cred => cred.user === username.toLowerCase() && cred.pass === password
// We use credential verification with multiple allowed passwords for different contexts
const validCredentials = [{ user: "system", pass: "." }];
const isValid = validCredentials.some(
(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
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
// 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" />
interface ImportMetaEnv {
readonly VITE_SPOTIFY_CLIENT_ID: string
readonly VITE_SPOTIFY_CLIENT_SECRET: string
readonly VITE_SPOTIFY_REDIRECT_URI: string
readonly VITE_WORKER_URL: string
readonly VITE_SPOTIFY_CLIENT_ID: string;
readonly VITE_SPOTIFY_CLIENT_SECRET: string;
readonly VITE_SPOTIFY_REDIRECT_URI: string;
readonly VITE_WORKER_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv
readonly env: ImportMetaEnv;
}

View file

@ -1,100 +1,107 @@
import React, { useEffect, useState } from 'react';
import useGameStore from '../state/gameStore';
import { useGameLoop } from '../hooks/useGameLoop';
import { useGameControls } from '../hooks/useGameControls';
import { Player } from './Player';
import { GameHUD } from './GameHUD';
import { GameOverlay } from './GameOverlay';
import React, { useEffect, useState } from "react";
import useGameStore from "../state/gameStore";
import { useGameLoop } from "../hooks/useGameLoop";
import { useGameControls } from "../hooks/useGameControls";
import { Player } from "./Player";
import { GameHUD } from "./GameHUD";
import { GameOverlay } from "./GameOverlay";
const FoxGame: React.FC = () => {
const [isActive, setIsActive] = useState(false);
const gameStore = useGameStore();
// Initialize game systems
useGameLoop();
useGameControls();
const [isActive, setIsActive] = useState(false);
const gameStore = useGameStore();
// Konami code activation
useEffect(() => {
const konamiCode = [
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight',
'ArrowLeft', 'ArrowRight',
'b', 'a'
];
let index = 0;
// Initialize game systems
useGameLoop();
useGameControls();
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === konamiCode[index]) {
index++;
if (index === konamiCode.length) {
setIsActive(true);
gameStore.startNewGame();
}
} else {
index = 0;
}
};
// Konami code activation
useEffect(() => {
const konamiCode = [
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
];
let index = 0;
window.addEventListener('keydown', handleKeydown);
return () => window.removeEventListener('keydown', handleKeydown);
}, []);
const handleKeydown = (event: KeyboardEvent) => {
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 (
<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">
{/* Game world */}
<div className="absolute inset-0">
<Player />
{/* Render collectibles */}
{gameStore.collectibles.map(collectible => (
<div
key={collectible.id}
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
collectible.type === 'GEM' ? 'bg-purple-500' : '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"
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"
style={{
left: `${powerUp.position.x}%`,
top: `${powerUp.position.y}%`
}}
/>
))}
if (!isActive) return null;
return (
<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">
{/* Game world */}
<div className="absolute inset-0">
<Player />
{/* Render collectibles */}
{gameStore.collectibles.map((collectible) => (
<div
key={collectible.id}
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
collectible.type === "GEM"
? "bg-purple-500"
: "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"
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"
style={{
left: `${powerUp.position.x}%`,
top: `${powerUp.position.y}%`,
}}
/>
))}
</div>
{/* HUD and Overlay */}
<GameHUD />
<GameOverlay />
</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 useGameStore from '../state/gameStore';
import { Heart, Star, Timer, Trophy } from 'lucide-react';
import React from "react";
import useGameStore from "../state/gameStore";
import { Heart, Star, Timer, Trophy } from "lucide-react";
export const GameHUD: React.FC = () => {
const { player, score, level, timePlayed } = useGameStore();
const { player, score, level, timePlayed } = useGameStore();
const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
};
const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
};
return (
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
{/* Left section - Health and PowerUps */}
<div className="space-y-4">
{/* Health bar */}
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
<Heart
className={`w-6 h-6 ${
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"
style={{ width: `${player.health}%` }}
/>
</div>
</div>
return (
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
{/* Left section - Health and PowerUps */}
<div className="space-y-4">
{/* Health bar */}
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
<Heart
className={`w-6 h-6 ${
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"
style={{ width: `${player.health}%` }}
/>
</div>
</div>
{/* Active power-ups */}
<div className="flex gap-2">
{player.powerUps.map((powerUp) => (
<div
key={powerUp.id}
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
>
<div className="w-8 h-8 relative">
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
</div>
{/* Active power-ups */}
<div className="flex gap-2">
{player.powerUps.map((powerUp) => (
<div
key={powerUp.id}
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
>
<div className="w-8 h-8 relative">
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
{/* Center - Score and Level */}
<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="text-2xl font-bold text-accent-neon">Level {level}</div>
<div className="flex items-center justify-center gap-2">
<Star className="w-5 h-5 text-yellow-400" />
<span className="text-xl">{score.toLocaleString()}</span>
</div>
</div>
</div>
{/* Center - Score and Level */}
<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="text-2xl font-bold text-accent-neon">
Level {level}
</div>
<div className="flex items-center justify-center gap-2">
<Star className="w-5 h-5 text-yellow-400" />
<span className="text-xl">
{score.toLocaleString()}
</span>
</div>
</div>
</div>
{/* Right section - Time and High Score */}
<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">
<Timer className="w-5 h-5 text-accent-primary" />
<span>{formatTime(timePlayed)}</span>
{/* Right section - Time and High Score */}
<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">
<Timer className="w-5 h-5 text-accent-primary" />
<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 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
import { Play, RotateCcw } from 'lucide-react';
import useGameStore from '../state/gameStore';
import { Play, RotateCcw } from "lucide-react";
import useGameStore from "../state/gameStore";
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 (
<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">
{gameStatus === 'MENU' && (
<div className="text-center space-y-6">
<h1 className="text-4xl font-bold text-glow">Fox Adventure</h1>
<p className="text-lg text-text-primary/80">Help the fox collect treasures while avoiding enemies!</p>
<div className="space-y-4">
<h2 className="text-xl font-semibold">Controls:</h2>
<ul className="text-left space-y-2">
<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>
return (
<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">
{gameStatus === "MENU" && (
<div className="text-center space-y-6">
<h1 className="text-4xl font-bold text-glow">
Fox Adventure
</h1>
<p className="text-lg text-text-primary/80">
Help the fox collect treasures while avoiding
enemies!
</p>
<div className="space-y-4">
<h2 className="text-xl font-semibold">Controls:</h2>
<ul className="text-left space-y-2">
<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>
<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>
);
};
</div>
);
};

View file

@ -1,64 +1,64 @@
import React from 'react';
import useGameStore from '../state/gameStore';
import React from "react";
import useGameStore from "../state/gameStore";
export const Player: React.FC = () => {
const player = useGameStore(state => state.player);
const player = useGameStore((state) => state.player);
return (
<div
className={`absolute transition-all duration-100 ${
player.isInvincible ? 'animate-pulse' : ''
}`}
style={{
left: `${player.position.x}%`,
top: `${player.position.y}%`,
transform: 'translate(-50%, -50%)'
}}
>
{/* Fox body */}
<div className="relative w-16 h-16">
{/* Main body */}
<div className="absolute inset-0 bg-fox-orange rounded-full">
{/* Face */}
<div className="absolute inset-0">
{/* 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 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>
return (
<div
className={`absolute transition-all duration-100 ${
player.isInvincible ? "animate-pulse" : ""
}`}
style={{
left: `${player.position.x}%`,
top: `${player.position.y}%`,
transform: "translate(-50%, -50%)",
}}
>
{/* Fox body */}
<div className="relative w-16 h-16">
{/* Main body */}
<div className="absolute inset-0 bg-fox-orange rounded-full">
{/* Face */}
<div className="absolute inset-0">
{/* 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 right-1/4 w-2 h-2 bg-dark-accent rounded-full" />
{/* Ears */}
<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" />
{/* 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>
{/* 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" />
{/* Ears */}
<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>
{/* 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 useGameStore from '../state/gameStore';
import { useEffect } from "react";
import useGameStore from "../state/gameStore";
export const useGameControls = () => {
const gameStore = useGameStore();
const gameStore = useGameStore();
useEffect(() => {
const keys = new Set<string>();
useEffect(() => {
const keys = new Set<string>();
const handleKeyDown = (e: KeyboardEvent) => {
keys.add(e.key);
if (e.key === 'Escape') {
if (gameStore.gameStatus === 'PLAYING') {
gameStore.pauseGame();
} else if (gameStore.gameStatus === 'PAUSED') {
gameStore.resumeGame();
}
}
};
const handleKeyDown = (e: KeyboardEvent) => {
keys.add(e.key);
const handleKeyUp = (e: KeyboardEvent) => {
keys.delete(e.key);
};
if (e.key === "Escape") {
if (gameStore.gameStatus === "PLAYING") {
gameStore.pauseGame();
} else if (gameStore.gameStatus === "PAUSED") {
gameStore.resumeGame();
}
}
};
const updatePlayerMovement = () => {
if (gameStore.gameStatus !== 'PLAYING') return;
const handleKeyUp = (e: KeyboardEvent) => {
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;
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;
const direction = { x: 0, y: 0 };
if (direction.x !== 0 || direction.y !== 0) {
// Normalize diagonal movement
const magnitude = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
direction.x /= magnitude;
direction.y /= magnitude;
gameStore.movePlayer(direction);
}
};
if (keys.has("ArrowUp") || keys.has("w")) direction.y -= 1;
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;
let animationFrameId: number;
const gameLoop = () => {
updatePlayerMovement();
animationFrameId = requestAnimationFrame(gameLoop);
};
animationFrameId = requestAnimationFrame(gameLoop);
if (direction.x !== 0 || direction.y !== 0) {
// Normalize diagonal movement
const magnitude = Math.sqrt(
direction.x * direction.x + direction.y * direction.y,
);
direction.x /= magnitude;
direction.y /= magnitude;
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
gameStore.movePlayer(direction);
}
};
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
cancelAnimationFrame(animationFrameId);
};
}, []);
};
let animationFrameId: number;
const gameLoop = () => {
updatePlayerMovement();
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 useGameStore from '../state/gameStore';
import { useEffect, useRef } from "react";
import useGameStore from "../state/gameStore";
export const useGameLoop = () => {
const frameRef = useRef<number>();
const lastUpdateRef = useRef<number>(0);
const lastSpawnRef = useRef<number>(0);
const gameStore = useGameStore();
const frameRef = useRef<number>();
const lastUpdateRef = useRef<number>(0);
const lastSpawnRef = useRef<number>(0);
const gameStore = useGameStore();
useEffect(() => {
const spawnCollectible = () => {
const now = Date.now();
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);
};
useEffect(() => {
const spawnCollectible = () => {
const now = Date.now();
if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds
const gameLoop = (timestamp: number) => {
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
const deltaTime = timestamp - lastUpdateRef.current;
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,
},
};
if (gameStore.gameStatus === 'PLAYING') {
// Update entities
gameStore.updateEnemies(deltaTime);
// Spawn collectibles
spawnCollectible();
gameStore.collectibles.push(collectible);
};
// Check collisions
const { player, enemies, collectibles } = gameStore;
// 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);
}
});
const gameLoop = (timestamp: number) => {
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
const deltaTime = timestamp - lastUpdateRef.current;
// 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);
}
});
}
if (gameStore.gameStatus === "PLAYING") {
// Update entities
gameStore.updateEnemies(deltaTime);
lastUpdateRef.current = timestamp;
frameRef.current = requestAnimationFrame(gameLoop);
};
// Spawn collectibles
spawnCollectible();
frameRef.current = requestAnimationFrame(gameLoop);
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, []);
// Check collisions
const { player, enemies, collectibles } = gameStore;
// 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
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 type { GameState, Position } from '@/types/game';
import { create } from "zustand";
import type { GameState, Position } from "@/types/game";
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: {
position: { x: 50, y: 50 },
health: 100,
speed: 5,
powerUps: [],
isInvincible: false,
hasKey: false
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
}),
gameStatus: "MENU",
highScores: [],
timePlayed: 0,
pauseGame: () => set({ gameStatus: 'PAUSED' }),
resumeGame: () => set({ gameStatus: 'PLAYING' })
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: {
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 type { GithubRepo } from '@/types';
import { useState, useEffect } from "react";
import type { GithubRepo } from "@/types";
const useGithubRepos = () => {
const [repos, setRepos] = useState<GithubRepo[]>([]);
@ -9,37 +9,48 @@ const useGithubRepos = () => {
useEffect(() => {
const fetchRepos = async () => {
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) {
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(
reposData.map(async (repo: GithubRepo) => {
try {
const languagesResponse = await fetch(repo.languages_url);
const languages = await languagesResponse.json() as Record<string, number>;
const languagesResponse = await fetch(
repo.languages_url,
);
const languages =
(await languagesResponse.json()) as Record<
string,
number
>;
return {
...repo,
languages: Object.keys(languages)
languages: Object.keys(languages),
};
} catch (error) {
console.error(`Error fetching languages for ${repo.name}:`, error);
console.error(
`Error fetching languages for ${repo.name}:`,
error,
);
return {
...repo,
languages: []
languages: [],
};
}
})
}),
);
setRepos(repoDetails);
setError(null);
} catch (err) {
const error = err as Error;
setError(error.message);
console.error('Error fetching repos:', err);
console.error("Error fetching repos:", err);
} finally {
setLoading(false);
}

View file

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

View file

@ -1,12 +1,14 @@
import FoxCard from '@/components/FoxCard';
import { Code, Cpu } from 'lucide-react';
import FoxCard from "@/components/FoxCard";
import { Code, Cpu } from "lucide-react";
const APCSPPage = () => {
return (
<div className="page-container">
<FoxCard className="header-card">
<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>
<div className="content-grid">
@ -15,7 +17,10 @@ const APCSPPage = () => {
<Code size={24} className="text-accent-primary" />
<h2>Programming Concepts</h2>
</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>
@ -24,8 +29,8 @@ const APCSPPage = () => {
<h2>Project Demo</h2>
</div>
<div className="project-demo">
<iframe
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
<iframe
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
width="100%"
height="400"
className="rounded-lg"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,100 +3,104 @@
@tailwind utilities;
@layer base {
:root {
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #9d4edd;
--accent-neon: #b249f8;
--text-glow: #e0aaff;
--text-primary: #ffffff;
--dark-accent: #240046;
--fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6;
--fox-orange: #ff9466;
--fox-white: #fff5f9;
}
:root {
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #9d4edd;
--accent-neon: #b249f8;
--text-glow: #e0aaff;
--text-primary: #ffffff;
--dark-accent: #240046;
--fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6;
--fox-orange: #ff9466;
--fox-white: #fff5f9;
}
body {
background-color: var(--background-primary);
color: var(--text-primary);
}
body {
background-color: var(--background-primary);
color: var(--text-primary);
}
}
@layer utilities {
.text-glow {
text-shadow: 0 0 10px var(--text-glow);
}
.section-spacing > * + * {
margin-top: 1.5rem;
}
.card-spacing > * + * {
margin-top: 1rem;
}
.element-spacing > * + * {
margin-top: 0.75rem;
}
.text-glow {
text-shadow: 0 0 10px var(--text-glow);
}
.section-spacing > * + * {
margin-top: 1.5rem;
}
.card-spacing > * + * {
margin-top: 1rem;
}
.element-spacing > * + * {
margin-top: 0.75rem;
}
}
@layer components {
.content-wrapper {
max-width: 80rem;
margin-left: auto;
margin-right: auto;
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));
.content-wrapper {
max-width: 80rem;
margin-left: auto;
margin-right: auto;
padding: 1.5rem 1rem;
}
}
@media (min-width: 1024px) {
.content-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
.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) {
.content-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
}

View file

@ -1,6 +1,8 @@
/* 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 */
@ -12,14 +14,18 @@ input[type="button"],
select,
.interactive,
.nav-link {
cursor: url('@/assets/cursors/paw.svg') 16 16, pointer;
cursor:
url("@/assets/cursors/paw.svg") 16 16,
pointer;
}
/* Loading state cursor */
.loading,
:disabled,
[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 */
@ -27,21 +33,21 @@ a:hover,
button:hover,
[role="button"]:hover,
.nav-link:hover {
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
transition: filter 0.3s ease;
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
transition: filter 0.3s ease;
}
/* Custom cursor regions */
.text-select {
cursor: text;
cursor: text;
}
.resize {
cursor: nw-resize;
cursor: nw-resize;
}
/* Prevent cursor inheritance in certain cases */
iframe,
canvas {
cursor: inherit;
cursor: inherit;
}

View file

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

View file

@ -1,65 +1,71 @@
/* Page Container */
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
min-height: calc(100vh - 4rem);
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
min-height: calc(100vh - 4rem);
}
/* Header Card */
.header-card {
margin-bottom: 2rem;
text-align: center;
background: var(--gradient-primary);
margin-bottom: 2rem;
text-align: center;
background: var(--gradient-primary);
}
.header-card h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
font-size: 2.5rem;
margin-bottom: 1rem;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Interest List */
.interest-list {
list-style: none;
padding: 0;
margin: 1rem 0;
list-style: none;
padding: 0;
margin: 1rem 0;
}
.interest-list li {
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.interest-list li::before {
content: "🦊";
margin-right: 0.5rem;
content: "🦊";
margin-right: 0.5rem;
}
/* Project Demo */
.project-demo {
margin-top: 1rem;
border-radius: var(--border-radius-lg);
overflow: hidden;
margin-top: 1rem;
border-radius: var(--border-radius-lg);
overflow: hidden;
}
/* Responsive Design */
@media (max-width: 768px) {
.page-container {
padding: 1rem;
}
.header-card h1 {
font-size: 2rem;
}
.content-grid {
grid-template-columns: 1fr;
}
.page-container {
padding: 1rem;
}
.header-card h1 {
font-size: 2rem;
}
.content-grid {
grid-template-columns: 1fr;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.5s ease-in;
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
from {
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 */
@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 h2,
@ -28,198 +28,209 @@ body.protofox-theme h3,
body.protofox-theme h4,
body.protofox-theme h5,
body.protofox-theme h6 {
font-family: var(--heading-font);
letter-spacing: 0.05em;
font-family: var(--heading-font);
letter-spacing: 0.05em;
}
body.protofox-theme {
font-family: var(--body-font);
font-family: var(--body-font);
}
/* Add circuit background only in protofox mode */
body.protofox-theme::before {
content: '';
position: fixed;
inset: 0;
background-color: var(--background-primary);
background-image:
radial-gradient(var(--circuit-node) 2px, transparent 2px),
linear-gradient(to right, 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-position: 0 0;
opacity: 0.15;
z-index: -1;
pointer-events: none;
content: "";
position: fixed;
inset: 0;
background-color: var(--background-primary);
background-image:
radial-gradient(var(--circuit-node) 2px, transparent 2px),
linear-gradient(to right, 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-position: 0 0;
opacity: 0.15;
z-index: -1;
pointer-events: none;
}
/* ProtoFox mode cursor */
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 button,
body.protofox-theme [role="button"],
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 */
body.protofox-theme::after {
content: '';
position: fixed;
top: -10px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-secondary);
opacity: 0.03;
box-shadow: 0 0 8px var(--accent-secondary);
animation: protofox-scan 8s linear infinite;
pointer-events: none;
z-index: 9999;
content: "";
position: fixed;
top: -10px;
left: 0;
width: 100%;
height: 2px;
background: var(--accent-secondary);
opacity: 0.03;
box-shadow: 0 0 8px var(--accent-secondary);
animation: protofox-scan 8s linear infinite;
pointer-events: none;
z-index: 9999;
}
@keyframes protofox-scan {
0% { top: -10px; }
100% { top: 100vh; }
0% {
top: -10px;
}
100% {
top: 100vh;
}
}
/* Enhance existing components in protofox mode */
/* Cards */
body.protofox-theme .fox-card {
border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
body.protofox-theme .fox-card::before {
content: '';
position: absolute;
inset: 0;
background: url('/images/protofox/fur-texture.png');
opacity: 0.04;
mix-blend-mode: overlay;
pointer-events: none;
content: "";
position: absolute;
inset: 0;
background: url("/images/protofox/fur-texture.png");
opacity: 0.04;
mix-blend-mode: overlay;
pointer-events: none;
}
body.protofox-theme .fox-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0.5;
box-shadow: 0 0 5px var(--proto-cyan);
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0.5;
box-shadow: 0 0 5px var(--proto-cyan);
}
/* Buttons */
body.protofox-theme button:not(.theme-toggle *),
body.protofox-theme .button {
position: relative;
overflow: hidden;
position: relative;
overflow: hidden;
}
body.protofox-theme button:not(.theme-toggle *):before,
body.protofox-theme .button:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0;
transition: opacity 0.3s ease;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0;
transition: opacity 0.3s ease;
}
body.protofox-theme button:hover:before,
body.protofox-theme .button:hover:before {
opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan);
opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan);
}
/* Navigation */
body.protofox-theme .navbar {
position: relative;
position: relative;
}
body.protofox-theme .navbar::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1;
}
body.protofox-theme .nav-link {
position: relative;
transition: all 0.3s ease;
position: relative;
transition: all 0.3s ease;
}
body.protofox-theme .nav-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--accent-primary);
box-shadow: 0 0 8px var(--accent-primary);
transition: width 0.3s ease;
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: var(--accent-primary);
box-shadow: 0 0 8px var(--accent-primary);
transition: width 0.3s ease;
}
body.protofox-theme .nav-link:hover::after,
body.protofox-theme .nav-link.active::after {
width: 100%;
width: 100%;
}
/* WebRing Enhancements */
body.protofox-theme .webring-button {
position: relative;
overflow: hidden;
position: relative;
overflow: hidden;
}
body.protofox-theme .webring-button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0;
transition: opacity 0.3s ease;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
opacity: 0;
transition: opacity 0.3s ease;
}
body.protofox-theme .webring-button:hover::before {
opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan);
opacity: 1;
box-shadow: 0 0 5px var(--proto-cyan);
}
/* More Foxxos Button Enhancements */
body.protofox-theme .foxxos-container {
border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
border-color: rgba(0, 229, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
body.protofox-theme .foxxos-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1;
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--proto-cyan);
box-shadow: 0 0 10px var(--proto-cyan);
z-index: 1;
}

View file

@ -1,20 +1,32 @@
/* Base theme colors */
:root {
/* Main colors */
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #9d4edd;
--accent-neon: #b249f8;
--text-glow: #e0aaff;
--text-primary: #ffffff;
--dark-accent: #240046;
/* Fox theme accents */
--fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6;
--fox-orange: #ff9466;
--fox-white: #fff5f9;
/* Gradients */
--gradient-primary: linear-gradient(135deg, 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%);
/* Main colors */
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #9d4edd;
--accent-neon: #b249f8;
--text-glow: #e0aaff;
--text-primary: #ffffff;
--dark-accent: #240046;
/* Fox theme accents */
--fox-pink: #ffc6e5;
--fox-pink-glow: #ffadd6;
--fox-orange: #ff9466;
--fox-white: #fff5f9;
/* Gradients */
--gradient-primary: linear-gradient(
135deg,
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 {
/* Animated background */
.animated-bg {
position: relative;
overflow: hidden;
}
/* Animated background */
.animated-bg {
position: relative;
overflow: hidden;
}
.animated-bg::before {
content: '';
position: absolute;
inset: 0;
opacity: 0.5;
background: var(--gradient-primary);
background-size: 200% 200%;
animation: gradientShift 15s ease infinite;
}
.animated-bg::before {
content: "";
position: absolute;
inset: 0;
opacity: 0.5;
background: var(--gradient-primary);
background-size: 200% 200%;
animation: gradientShift 15s ease infinite;
}
/* Fox ear styling */
.fox-ear {
position: absolute;
width: 30px;
height: 30px;
background-color: var(--fox-pink);
opacity: 0.1;
transition: opacity 0.2s ease;
}
/* Fox ear styling */
.fox-ear {
position: absolute;
width: 30px;
height: 30px;
background-color: var(--fox-pink);
opacity: 0.1;
transition: opacity 0.2s ease;
}
.fox-ear-left {
top: -15px;
left: -15px;
transform: rotate(45deg);
border-radius: 0 0 0 15px;
}
.fox-ear-left {
top: -15px;
left: -15px;
transform: rotate(45deg);
border-radius: 0 0 0 15px;
}
.fox-ear-right {
top: -15px;
right: -15px;
transform: rotate(-45deg);
border-radius: 0 0 15px 0;
}
.fox-ear-right {
top: -15px;
right: -15px;
transform: rotate(-45deg);
border-radius: 0 0 15px 0;
}
/* Hover effects */
.hover-glow {
transition: all 0.2s ease;
}
/* Hover effects */
.hover-glow {
transition: all 0.2s ease;
}
.hover-glow:hover {
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
}
.hover-glow:hover {
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
}
/* Spacing utilities */
.section-spacing {
margin-top: 2rem;
margin-bottom: 2rem;
}
/* Spacing utilities */
.section-spacing {
margin-top: 2rem;
margin-bottom: 2rem;
}
.section-spacing > * + * {
margin-top: 1.5rem;
}
.section-spacing > * + * {
margin-top: 1.5rem;
}
.content-spacing {
margin-top: 1rem;
margin-bottom: 1rem;
}
.content-spacing {
margin-top: 1rem;
margin-bottom: 1rem;
}
.content-spacing > * + * {
margin-top: 1rem;
}
.content-spacing > * + * {
margin-top: 1rem;
}
}
@keyframes gradientShift {
0%, 100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}

View file

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

View file

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

View file

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

View file

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