mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 16:28:16 +00:00
Formating and small updates
This commit is contained in:
parent
9ae7b63d2d
commit
8c877c2358
52 changed files with 7433 additions and 2968 deletions
13
README.md
13
README.md
|
|
@ -61,20 +61,7 @@ npm start
|
||||||
|
|
||||||
The application will be available at http://localhost:3000
|
The application will be available at http://localhost:3000
|
||||||
|
|
||||||
### Component Structure
|
|
||||||
|
|
||||||
The project follows a modular component structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── components/
|
|
||||||
│ ├── LastFMTrack/ # Music integration
|
|
||||||
│ ├── GithubRepos/ # GitHub repository display
|
|
||||||
│ ├── LoadingFox/ # Loading states
|
|
||||||
│ └── ParallaxEffect/ # Visual effects
|
|
||||||
├── App.tsx # Main application component
|
|
||||||
└── index.tsx # Application entry point
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Deployment
|
## 🌐 Deployment
|
||||||
|
|
||||||
|
|
|
||||||
63
index.html
63
index.html
|
|
@ -1,28 +1,39 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap" rel="stylesheet">
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>EndofTimee</title>
|
|
||||||
<meta name="description" content="EndofTimee's personal site – ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
|
|
||||||
<meta property="og:title" content="EndofTimee – ProtoFox Personal Site" />
|
|
||||||
<meta property="og:description" content="EndofTimee's personal site – ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://endoftimee.com/" />
|
|
||||||
<meta property="og:image" content="/logo.svg" />
|
|
||||||
<meta property="og:image:alt" content="Fox Logo" />
|
|
||||||
<meta property="og:site_name" content="EndofTimee" />
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:title" content="EndofTimee – ProtoFox Personal Site" />
|
|
||||||
<meta name="twitter:description" content="EndofTimee's personal site – ProtoFox, developer, streamer, and more. Featuring a unique boot animation and fox-themed design." />
|
|
||||||
<meta name="twitter:image" content="/logo.svg" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>EndofTimee</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="EndofTimee's personal site – developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
|
||||||
|
/>
|
||||||
|
<meta property="og:title" content="EndofTimee – Personal Site" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="EndofTimee's personal site – developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
|
||||||
|
/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://endoftimee.com/" />
|
||||||
|
<meta property="og:image" content="/logo.svg" />
|
||||||
|
<meta property="og:image:alt" content="Fox Logo" />
|
||||||
|
<meta property="og:site_name" content="EndofTimee" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="EndofTimee – Personal Site" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="EndofTimee's personal site – Developer, streamer, and more. Featuring a unique boot animation and fox-themed design."
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content="/logo.svg" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
||||||
4637
package-lock.json
generated
4637
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
"eslint-config-react-app": "7.0.1",
|
||||||
"itty-router": "^4.0.27",
|
"itty-router": "^4.0.27",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|
@ -39,14 +40,16 @@
|
||||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"@vitest/ui": "^1.2.2",
|
"@vitest/ui": "^1.2.2",
|
||||||
"autoprefixer": "10.4.20",
|
"autoprefixer": "10.4.20",
|
||||||
|
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "6.0.0-rc.2",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"husky": "^9.0.10",
|
"husky": "^9.0.10",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"lucide-react": "0.474.0",
|
"lucide-react": "0.474.0",
|
||||||
|
"plugin-react": "0.0.1-security",
|
||||||
"postcss": "8.5.1",
|
"postcss": "8.5.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
|
|
@ -54,6 +57,7 @@
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"vite": "6.2.2",
|
"vite": "6.2.2",
|
||||||
|
"vite-plugin-babel": "1.3.2",
|
||||||
"vitest": "3.0.9",
|
"vitest": "3.0.9",
|
||||||
"web-vitals": "4.2.4",
|
"web-vitals": "4.2.4",
|
||||||
"wrangler": "^3.28.0"
|
"wrangler": "^3.28.0"
|
||||||
|
|
|
||||||
389
src/App.tsx
389
src/App.tsx
|
|
@ -4,113 +4,120 @@ import { AuthProvider, useAuth } from "@/context/AuthContext";
|
||||||
import Navbar from "@/components/Navbar";
|
import Navbar from "@/components/Navbar";
|
||||||
import AboutPage from "@/pages/AboutPage";
|
import AboutPage from "@/pages/AboutPage";
|
||||||
import ProjectsPage from "@/pages/ProjectsPage";
|
import ProjectsPage from "@/pages/ProjectsPage";
|
||||||
import APCSPPage from "@/pages/APCSPPage";
|
// import APCSPPage from "@/pages/APCSPPage";
|
||||||
// import LoginPage from "@/pages/LoginPage";
|
// import LoginPage from "@/pages/LoginPage";
|
||||||
// import SystemPage from "@/pages/SystemPage";
|
// import SystemPage from "@/pages/SystemPage";
|
||||||
import VNCViewer from "@/components/VNCViewer";
|
|
||||||
// import SystemStatus from "@/components/SystemStatus";
|
// import SystemStatus from "@/components/SystemStatus";
|
||||||
// import SwitchNotification from "@/components/SwitchNotification";
|
// import SwitchNotification from "@/components/SwitchNotification";
|
||||||
// import ProtectedRoute from "@/components/ProtectedRoute";
|
// import ProtectedRoute from "@/components/ProtectedRoute";
|
||||||
import FoxGame from "@/games/fox-adventure/components/FoxGame";
|
import FoxGame from "@/games/fox-adventure/components/FoxGame";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import EndOSBootAnimation from "@/components/EndOSBootAnimation";
|
import EndOSBootAnimation from "@/components/EndOSBootAnimation";
|
||||||
import '@/styles/animations.css';
|
import "@/styles/animations.css";
|
||||||
import '@/styles/protofox-theme.css';
|
import "@/styles/protofox-theme.css";
|
||||||
|
|
||||||
// EndOS animation control
|
// EndOS animation control
|
||||||
const useEndOSAnimation = () => {
|
const useEndOSAnimation = () => {
|
||||||
const [bootComplete, setBootComplete] = useState(false);
|
const [bootComplete, setBootComplete] = useState(false);
|
||||||
// Using underscore prefix to indicate intentionally unused variable
|
// Using underscore prefix to indicate intentionally unused variable
|
||||||
const [skipBoot, _setSkipBoot] = useState(() => {
|
const [skipBoot, _setSkipBoot] = useState(() => {
|
||||||
// Check for URL parameter that allows skipping the boot animation
|
// Check for URL parameter that allows skipping the boot animation
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const skipParam = urlParams.get('skipBoot');
|
const skipParam = urlParams.get("skipBoot");
|
||||||
|
|
||||||
// Also check if user has already seen the animation in this session
|
// Also check if user has already seen the animation in this session
|
||||||
const sessionSeen = sessionStorage.getItem('endos-boot-complete') === 'true';
|
const sessionSeen =
|
||||||
|
sessionStorage.getItem("endos-boot-complete") === "true";
|
||||||
return skipParam === 'true' || sessionSeen;
|
|
||||||
});
|
return skipParam === "true" || sessionSeen;
|
||||||
|
});
|
||||||
// Handle boot animation completion
|
|
||||||
const handleBootComplete = () => {
|
// Handle boot animation completion
|
||||||
setBootComplete(true);
|
const handleBootComplete = () => {
|
||||||
sessionStorage.setItem('endos-boot-complete', 'true');
|
setBootComplete(true);
|
||||||
};
|
sessionStorage.setItem("endos-boot-complete", "true");
|
||||||
|
};
|
||||||
return { bootComplete, skipBoot, handleBootComplete };
|
|
||||||
|
return { bootComplete, skipBoot, handleBootComplete };
|
||||||
};
|
};
|
||||||
|
|
||||||
// AuthChecker component to access auth context inside the router
|
// AuthChecker component to access auth context inside the router
|
||||||
const AuthChecker = ({ children }: { children: React.ReactNode }) => {
|
const AuthChecker = ({ children }: { children: React.ReactNode }) => {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [isStatusVisible, setIsStatusVisible] = useState(false);
|
const [isStatusVisible, setIsStatusVisible] = useState(false);
|
||||||
// Using underscore prefix for all unused state variables
|
// Using underscore prefix for all unused state variables
|
||||||
const [_showNotification, setShowNotification] = useState(false);
|
const [_showNotification, setShowNotification] = useState(false);
|
||||||
const [_notificationType, setNotificationType] = useState<'switch' | 'warning' | 'notice'>('switch');
|
const [_notificationType, setNotificationType] = useState<
|
||||||
const [_notificationMessage, setNotificationMessage] = useState('');
|
"switch" | "warning" | "notice"
|
||||||
const [_selectedAlter, setSelectedAlter] = useState('');
|
>("switch");
|
||||||
|
const [_notificationMessage, setNotificationMessage] = useState("");
|
||||||
|
const [_selectedAlter, setSelectedAlter] = useState("");
|
||||||
|
|
||||||
// Toggle system status floating panel
|
// Toggle system status floating panel
|
||||||
const toggleStatus = () => {
|
const toggleStatus = () => {
|
||||||
setIsStatusVisible(prev => !prev);
|
setIsStatusVisible((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simulate random switches for demo purposes
|
// Simulate random switches for demo purposes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!auth.isAuthenticated) return;
|
if (!auth.isAuthenticated) return;
|
||||||
|
|
||||||
// Every 5-15 minutes, show a switch notification
|
// Every 5-15 minutes, show a switch notification
|
||||||
const randomInterval = Math.floor(Math.random() * (15 - 5 + 1) + 5) * 60 * 1000;
|
const randomInterval =
|
||||||
const interval = setInterval(() => {
|
Math.floor(Math.random() * (15 - 5 + 1) + 5) * 60 * 1000;
|
||||||
// 70% chance of switch, 20% chance of notice, 10% chance of warning
|
const interval = setInterval(() => {
|
||||||
const rand = Math.random();
|
// 70% chance of switch, 20% chance of notice, 10% chance of warning
|
||||||
|
const rand = Math.random();
|
||||||
if (rand < 0.7) {
|
|
||||||
setNotificationType('switch');
|
|
||||||
setSelectedAlter(''); // Random alter will be selected
|
|
||||||
} else if (rand < 0.9) {
|
|
||||||
setNotificationType('notice');
|
|
||||||
setNotificationMessage('System communication active');
|
|
||||||
} else {
|
|
||||||
setNotificationType('warning');
|
|
||||||
setNotificationMessage('System experiencing stress');
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowNotification(true);
|
|
||||||
}, randomInterval);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
if (rand < 0.7) {
|
||||||
}, [auth.isAuthenticated]);
|
setNotificationType("switch");
|
||||||
|
setSelectedAlter(""); // Random alter will be selected
|
||||||
|
} else if (rand < 0.9) {
|
||||||
|
setNotificationType("notice");
|
||||||
|
setNotificationMessage("System communication active");
|
||||||
|
} else {
|
||||||
|
setNotificationType("warning");
|
||||||
|
setNotificationMessage("System experiencing stress");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
setShowNotification(true);
|
||||||
<>
|
}, randomInterval);
|
||||||
{children}
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
{/* Floating System Status for authenticated users */}
|
}, [auth.isAuthenticated]);
|
||||||
{auth.isAuthenticated && (
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
{children}
|
||||||
className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
|
|
||||||
isStatusVisible ? 'translate-y-0' : 'translate-y-[calc(100%-40px)]'
|
{/* Floating System Status for authenticated users */}
|
||||||
}`}
|
{auth.isAuthenticated && (
|
||||||
>
|
<>
|
||||||
<div
|
<div
|
||||||
className="p-2 bg-background-secondary rounded-t-lg cursor-pointer flex justify-center items-center"
|
className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
|
||||||
onClick={toggleStatus}
|
isStatusVisible
|
||||||
>
|
? "translate-y-0"
|
||||||
<span className="text-xs font-medium">
|
: "translate-y-[calc(100%-40px)]"
|
||||||
{isStatusVisible ? "Hide System Status" : "System Status"}
|
}`}
|
||||||
</span>
|
>
|
||||||
</div>
|
<div
|
||||||
{/* <SystemStatus
|
className="p-2 bg-background-secondary rounded-t-lg cursor-pointer flex justify-center items-center"
|
||||||
|
onClick={toggleStatus}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-medium">
|
||||||
|
{isStatusVisible
|
||||||
|
? "Hide System Status"
|
||||||
|
: "System Status"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* <SystemStatus
|
||||||
minimal={true}
|
minimal={true}
|
||||||
className="shadow-lg rounded-t-none w-[300px] max-w-[calc(100vw-2rem)]"
|
className="shadow-lg rounded-t-none w-[300px] max-w-[calc(100vw-2rem)]"
|
||||||
/> */}
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* System Notifications */}
|
{/* System Notifications */}
|
||||||
{/* <SwitchNotification
|
{/* <SwitchNotification
|
||||||
show={showNotification}
|
show={showNotification}
|
||||||
onClose={() => setShowNotification(false)}
|
onClose={() => setShowNotification(false)}
|
||||||
alterName={selectedAlter}
|
alterName={selectedAlter}
|
||||||
|
|
@ -119,10 +126,10 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
|
||||||
autoClose
|
autoClose
|
||||||
autoCloseDelay={5000}
|
autoCloseDelay={5000}
|
||||||
/> */}
|
/> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
@ -132,26 +139,31 @@ const App = () => {
|
||||||
|
|
||||||
// Demo the switch notification after a delay
|
// Demo the switch notification after a delay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
setShowInitialSwitchDemo(true);
|
setShowInitialSwitchDemo(true);
|
||||||
|
|
||||||
// Hide after 5 seconds
|
// Hide after 5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowInitialSwitchDemo(false);
|
setShowInitialSwitchDemo(false);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Konami code sequence
|
// Konami code sequence
|
||||||
const konamiCode = [
|
const konamiCode = [
|
||||||
'ArrowUp', 'ArrowUp',
|
"ArrowUp",
|
||||||
'ArrowDown', 'ArrowDown',
|
"ArrowUp",
|
||||||
'ArrowLeft', 'ArrowRight',
|
"ArrowDown",
|
||||||
'ArrowLeft', 'ArrowRight',
|
"ArrowDown",
|
||||||
'b', 'a'
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"ArrowLeft",
|
||||||
|
"ArrowRight",
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
];
|
];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
|
|
@ -159,12 +171,12 @@ const App = () => {
|
||||||
// Check if the pressed key matches the next key in the sequence
|
// Check if the pressed key matches the next key in the sequence
|
||||||
if (event.key === konamiCode[index]) {
|
if (event.key === konamiCode[index]) {
|
||||||
index++;
|
index++;
|
||||||
|
|
||||||
// If the entire sequence is completed
|
// If the entire sequence is completed
|
||||||
if (index === konamiCode.length) {
|
if (index === konamiCode.length) {
|
||||||
setIsGameActive(true);
|
setIsGameActive(true);
|
||||||
// Optional: Play a sound or show a notification
|
// Optional: Play a sound or show a notification
|
||||||
console.log('Konami code activated!');
|
console.log("Konami code activated!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset if a wrong key is pressed
|
// Reset if a wrong key is pressed
|
||||||
|
|
@ -172,55 +184,64 @@ const App = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeydown);
|
window.addEventListener("keydown", handleKeydown);
|
||||||
|
|
||||||
// Clean up the event listener on component unmount
|
// Clean up the event listener on component unmount
|
||||||
return () => window.removeEventListener('keydown', handleKeydown);
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
{/* EndOS Boot Animation */}
|
{/* EndOS Boot Animation */}
|
||||||
{!skipBoot && !bootComplete && (
|
{!skipBoot && !bootComplete && (
|
||||||
<EndOSBootAnimation onComplete={handleBootComplete} />
|
<EndOSBootAnimation onComplete={handleBootComplete} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Application - Only visible after boot animation completes */}
|
{/* Main Application - Only visible after boot animation completes */}
|
||||||
<div style={{
|
<div
|
||||||
visibility: bootComplete || skipBoot ? 'visible' : 'hidden',
|
style={{
|
||||||
opacity: bootComplete || skipBoot ? 1 : 0,
|
visibility: bootComplete || skipBoot ? "visible" : "hidden",
|
||||||
transition: 'opacity 0.5s ease-in-out'
|
opacity: bootComplete || skipBoot ? 1 : 0,
|
||||||
}}>
|
transition: "opacity 0.5s ease-in-out",
|
||||||
<Router>
|
}}
|
||||||
<AuthChecker>
|
>
|
||||||
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}>
|
<Router>
|
||||||
{/* Background Logo */}
|
<AuthChecker>
|
||||||
<div className="fixed inset-0 z-behind pointer-events-none">
|
<div
|
||||||
<div className="absolute inset-0">
|
className={`min-h-screen bg-background-primary ${isGameActive ? "game-active" : ""}`}
|
||||||
<img
|
>
|
||||||
src="/logo.jpg"
|
{/* Background Logo */}
|
||||||
alt="Background Logo"
|
<div className="fixed inset-0 z-behind pointer-events-none">
|
||||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]"
|
<div className="absolute inset-0">
|
||||||
/>
|
<img
|
||||||
</div>
|
src="/logo.jpg"
|
||||||
</div>
|
alt="Background Logo"
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]"
|
||||||
{/* Main Content */}
|
/>
|
||||||
<div className="relative">
|
</div>
|
||||||
<Navbar />
|
</div>
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Main Content */}
|
||||||
<div className="fixed top-20 right-4 z-30">
|
<div className="relative">
|
||||||
<ThemeToggle />
|
<Navbar />
|
||||||
</div>
|
|
||||||
|
{/* Theme Toggle */}
|
||||||
<main className="content-wrapper section-spacing pb-20 animate-fade-in">
|
<div className="fixed top-20 right-4 z-30">
|
||||||
<Routes>
|
<ThemeToggle />
|
||||||
<Route path="/" element={<AboutPage />} />
|
</div>
|
||||||
<Route path="/projects" element={<ProjectsPage />} />
|
|
||||||
<Route path="/apcsp" element={<APCSPPage />} />
|
<main className="content-wrapper section-spacing pb-20 animate-fade-in">
|
||||||
<Route path="/vnc" element={<VNCViewer />} />
|
<Routes>
|
||||||
{/* <Route path="/login" element={<LoginPage />} />
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={<AboutPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/projects"
|
||||||
|
element={<ProjectsPage />}
|
||||||
|
/>
|
||||||
|
{/* <Route path="/apcsp" element={<APCSPPage />} /> */}
|
||||||
|
{/* <Route path="/login" element={<LoginPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/system"
|
path="/system"
|
||||||
element={
|
element={
|
||||||
|
|
@ -229,46 +250,58 @@ const App = () => {
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/> */}
|
/> */}
|
||||||
<Route path="*" element={
|
<Route
|
||||||
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
|
path="*"
|
||||||
<h1 className="text-4xl font-bold text-glow">404: Page Not Found</h1>
|
element={
|
||||||
<p className="text-xl text-text-primary/80">This fox couldn't find what you're looking for.</p>
|
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
|
||||||
</div>
|
<h1 className="text-4xl font-bold text-glow">
|
||||||
} />
|
404: Page Not Found
|
||||||
</Routes>
|
</h1>
|
||||||
</main>
|
<p className="text-xl text-text-primary/80">
|
||||||
|
This fox couldn't find
|
||||||
{/* Footer */}
|
what you're looking for.
|
||||||
<footer className="py-6 border-t border-accent-primary/10 text-center text-sm text-text-primary/60">
|
</p>
|
||||||
<p>© 2023 - {new Date().getFullYear()} EndofTimee. All rights reserved.</p>
|
</div>
|
||||||
<div className="flex justify-center items-center gap-2 mt-2">
|
}
|
||||||
<span className="text-xs">Try the Konami code: ↑↑↓↓←→←→BA</span>
|
/>
|
||||||
<div className="bg-background-secondary px-2 py-0.5 rounded-full text-[10px] text-accent-primary">
|
</Routes>
|
||||||
v1.3.0
|
</main>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="py-6 border-t border-accent-primary/10 text-center text-sm text-text-primary/60">
|
||||||
|
<p>
|
||||||
|
© 2023 - {new Date().getFullYear()}{" "}
|
||||||
|
EndofTimee. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center items-center gap-2 mt-2">
|
||||||
|
<span className="text-xs">
|
||||||
|
Try the Konami code: ↑↑↓↓←→←→BA
|
||||||
|
</span>
|
||||||
|
<div className="bg-background-secondary px-2 py-0.5 rounded-full text-[10px] text-accent-primary">
|
||||||
|
v0.9.5
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Fox Game Overlay - Activated by Konami Code */}
|
{/* Fox Game Overlay - Activated by Konami Code */}
|
||||||
{isGameActive && (
|
{isGameActive && (
|
||||||
<>
|
<>
|
||||||
<FoxGame />
|
<FoxGame />
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsGameActive(false)}
|
onClick={() => setIsGameActive(false)}
|
||||||
className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors"
|
className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Exit Game
|
Exit Game
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AuthChecker>
|
</AuthChecker>
|
||||||
</Router>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -1,215 +1,269 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import '@/styles/EndOSBootAnimation.css';
|
import "@/styles/EndOSBootAnimation.css";
|
||||||
|
|
||||||
interface EndOSBootAnimationProps {
|
interface EndOSBootAnimationProps {
|
||||||
// Using underscores to mark unused props to avoid TypeScript errors
|
// Using underscores to mark unused props to avoid TypeScript errors
|
||||||
_customLogo?: string;
|
_customLogo?: string;
|
||||||
_customColors?: {
|
_customColors?: {
|
||||||
primary?: string;
|
primary?: string;
|
||||||
secondary?: string;
|
secondary?: string;
|
||||||
fox?: string;
|
fox?: string;
|
||||||
};
|
};
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
skipAnimation?: boolean;
|
skipAnimation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
|
const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
|
||||||
onComplete,
|
onComplete,
|
||||||
skipAnimation = false,
|
skipAnimation = false,
|
||||||
// Rename with underscores to indicate variables are intentionally unused
|
// Rename with underscores to indicate variables are intentionally unused
|
||||||
_customLogo,
|
_customLogo,
|
||||||
_customColors
|
_customColors,
|
||||||
}) => {
|
}) => {
|
||||||
const [active, setActive] = useState(true);
|
const [active, setActive] = useState(true);
|
||||||
const [bootStage, setBootStage] = useState(0);
|
const [bootStage, setBootStage] = useState(0);
|
||||||
const [showLogo, setShowLogo] = useState(false);
|
const [showLogo, setShowLogo] = useState(false);
|
||||||
const [bootComplete, setBootComplete] = useState(false);
|
const [bootComplete, setBootComplete] = useState(false);
|
||||||
|
|
||||||
// Boot sequence timing
|
// Boot sequence timing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (skipAnimation) {
|
if (skipAnimation) {
|
||||||
handleAnimationComplete();
|
handleAnimationComplete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize boot sequence with proper timing to prevent overlap
|
// Initialize boot sequence with proper timing to prevent overlap
|
||||||
const bootSequence = [
|
const bootSequence = [
|
||||||
{ stage: 1, delay: 1500 }, // Initial screen flicker
|
{ stage: 1, delay: 1500 }, // Initial screen flicker
|
||||||
{ stage: 2, delay: 3000 }, // BIOS check (longer time to read)
|
{ stage: 2, delay: 3000 }, // BIOS check (longer time to read)
|
||||||
{ stage: 3, delay: 3500 }, // System scan (longer for progress bar)
|
{ stage: 3, delay: 3500 }, // System scan (longer for progress bar)
|
||||||
{ stage: 4, delay: 3500 }, // Loading modules (allow time for animation)
|
{ stage: 4, delay: 3500 }, // Loading modules (allow time for animation)
|
||||||
{ stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits)
|
{ stage: 5, delay: 3500 }, // Fox protocols (allow time to read traits)
|
||||||
{ stage: 6, delay: 3000 }, // Show logo (allow time to appreciate)
|
{ stage: 6, delay: 3000 }, // Show logo (allow time to appreciate)
|
||||||
{ stage: 7, delay: 3000 }, // Final activation (longer read time)
|
{ stage: 7, delay: 3000 }, // Final activation (longer read time)
|
||||||
{ stage: 8, delay: 2000 } // Fade out
|
{ stage: 8, delay: 2000 }, // Fade out
|
||||||
];
|
];
|
||||||
|
|
||||||
let timeout: any; // Using any instead of NodeJS.Timeout
|
let timeout: any; // Using any instead of NodeJS.Timeout
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
|
|
||||||
const runNextStage = () => {
|
const runNextStage = () => {
|
||||||
if (currentIndex < bootSequence.length) {
|
if (currentIndex < bootSequence.length) {
|
||||||
const { stage, delay } = bootSequence[currentIndex];
|
const { stage, delay } = bootSequence[currentIndex];
|
||||||
|
|
||||||
// Clean transition - clear ALL previous stages to prevent any background visibility
|
// Clean transition - clear ALL previous stages to prevent any background visibility
|
||||||
document.querySelectorAll('.boot-stage').forEach(el => {
|
document.querySelectorAll(".boot-stage").forEach((el) => {
|
||||||
el.classList.remove('active');
|
el.classList.remove("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset content visibility
|
// Reset content visibility
|
||||||
document.querySelectorAll('.boot-content').forEach(el => {
|
document.querySelectorAll(".boot-content").forEach((el) => {
|
||||||
el.classList.remove('active');
|
el.classList.remove("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Short delay to allow for transition
|
// Short delay to allow for transition
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// First ensure boot content is visible
|
// First ensure boot content is visible
|
||||||
document.querySelectorAll('.boot-content').forEach(el => {
|
document.querySelectorAll(".boot-content").forEach((el) => {
|
||||||
el.classList.add('active');
|
el.classList.add("active");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then activate the correct stage
|
// Then activate the correct stage
|
||||||
setBootStage(stage);
|
setBootStage(stage);
|
||||||
|
|
||||||
if (stage === 6) {
|
if (stage === 6) {
|
||||||
setShowLogo(true);
|
setShowLogo(true);
|
||||||
} else if (stage === 7) {
|
} else if (stage === 7) {
|
||||||
setBootComplete(true);
|
setBootComplete(true);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
currentIndex++;
|
currentIndex++;
|
||||||
timeout = setTimeout(runNextStage, delay);
|
timeout = setTimeout(runNextStage, delay);
|
||||||
} else {
|
} else {
|
||||||
|
handleAnimationComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the sequence
|
||||||
|
timeout = setTimeout(runNextStage, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [skipAnimation]);
|
||||||
|
|
||||||
|
const handleAnimationComplete = () => {
|
||||||
|
setActive(false);
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip button click handler
|
||||||
|
const handleSkip = () => {
|
||||||
handleAnimationComplete();
|
handleAnimationComplete();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start the sequence
|
if (!active) return null;
|
||||||
timeout = setTimeout(runNextStage, 500);
|
|
||||||
|
return (
|
||||||
return () => {
|
<div className="endos-boot-container">
|
||||||
if (timeout) clearTimeout(timeout);
|
{/* Accessibility */}
|
||||||
};
|
<div className="visually-hidden">
|
||||||
}, [skipAnimation]);
|
Website loading. EndOS boot sequence in progress.
|
||||||
|
|
||||||
const handleAnimationComplete = () => {
|
|
||||||
setActive(false);
|
|
||||||
if (onComplete) {
|
|
||||||
onComplete();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Skip button click handler
|
|
||||||
const handleSkip = () => {
|
|
||||||
handleAnimationComplete();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!active) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="endos-boot-container">
|
|
||||||
{/* Accessibility */}
|
|
||||||
<div className="visually-hidden">
|
|
||||||
Website loading. EndOS boot sequence in progress.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="boot-scan-line"></div>
|
|
||||||
|
|
||||||
<div className="boot-visor-frame">
|
|
||||||
<div className="visor-left-ear"></div>
|
|
||||||
<div className="visor-right-ear"></div>
|
|
||||||
|
|
||||||
<div className="boot-visor">
|
|
||||||
<div className="visor-line top"></div>
|
|
||||||
<div className="visor-line bottom"></div>
|
|
||||||
|
|
||||||
{/* Boot Sequence Content */}
|
|
||||||
<div className={`boot-content`}>
|
|
||||||
{/* BIOS Check */}
|
|
||||||
<div className={`boot-stage bios ${bootStage === 2 ? 'active' : ''}`}>
|
|
||||||
<div className="bios-header">END_OS BIOS v2.5</div>
|
|
||||||
<div className="boot-text-line">Initializing hardware...</div>
|
|
||||||
<div className="boot-text-line">CPU: ProtoCore i9 @ 4.7GHz</div>
|
|
||||||
<div className="boot-text-line">Memory: 16GB NeuralRAM</div>
|
|
||||||
<div className="boot-text-line">Checking system integrity... OK</div>
|
|
||||||
<div className="boot-text-line">Starting boot sequence...</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* System Scan */}
|
<div className="boot-scan-line"></div>
|
||||||
<div className={`boot-stage scan ${bootStage === 3 ? 'active' : ''}`}>
|
|
||||||
<div className="scan-header">SYSTEM SCAN</div>
|
<div className="boot-visor-frame">
|
||||||
<div className="scan-progress-container">
|
<div className="visor-left-ear"></div>
|
||||||
<div className="scan-progress-bar"></div>
|
<div className="visor-right-ear"></div>
|
||||||
</div>
|
|
||||||
<div className="scan-detail">Checking vital systems...</div>
|
<div className="boot-visor">
|
||||||
<div className="scan-detail">Initializing neural pathways...</div>
|
<div className="visor-line top"></div>
|
||||||
<div className="scan-detail">Activating sensory modules...</div>
|
<div className="visor-line bottom"></div>
|
||||||
<div className="scan-detail">All systems nominal</div>
|
|
||||||
</div>
|
{/* Boot Sequence Content */}
|
||||||
|
<div className={`boot-content`}>
|
||||||
{/* Module Loading */}
|
{/* BIOS Check */}
|
||||||
<div className={`boot-stage modules ${bootStage === 4 ? 'active' : ''}`}>
|
<div
|
||||||
<div className="module-header">LOADING CORE MODULES</div>
|
className={`boot-stage bios ${bootStage === 2 ? "active" : ""}`}
|
||||||
<div className="modules-grid">
|
>
|
||||||
<div className="module-item">
|
<div className="bios-header">END_OS BIOS v2.5</div>
|
||||||
<div className="module-icon"></div>
|
<div className="boot-text-line">
|
||||||
<div className="module-name">ProtogenCore</div>
|
Initializing hardware...
|
||||||
|
</div>
|
||||||
|
<div className="boot-text-line">
|
||||||
|
CPU: ProtoCore i9 @ 4.7GHz
|
||||||
|
</div>
|
||||||
|
<div className="boot-text-line">
|
||||||
|
Memory: 16GB NeuralRAM
|
||||||
|
</div>
|
||||||
|
<div className="boot-text-line">
|
||||||
|
Checking system integrity... OK
|
||||||
|
</div>
|
||||||
|
<div className="boot-text-line">
|
||||||
|
Starting boot sequence...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* System Scan */}
|
||||||
|
<div
|
||||||
|
className={`boot-stage scan ${bootStage === 3 ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="scan-header">SYSTEM SCAN</div>
|
||||||
|
<div className="scan-progress-container">
|
||||||
|
<div className="scan-progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div className="scan-detail">
|
||||||
|
Checking vital systems...
|
||||||
|
</div>
|
||||||
|
<div className="scan-detail">
|
||||||
|
Initializing neural pathways...
|
||||||
|
</div>
|
||||||
|
<div className="scan-detail">
|
||||||
|
Activating sensory modules...
|
||||||
|
</div>
|
||||||
|
<div className="scan-detail">
|
||||||
|
All systems nominal
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Module Loading */}
|
||||||
|
<div
|
||||||
|
className={`boot-stage modules ${bootStage === 4 ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="module-header">
|
||||||
|
LOADING CORE MODULES
|
||||||
|
</div>
|
||||||
|
<div className="modules-grid">
|
||||||
|
<div className="module-item">
|
||||||
|
<div className="module-icon"></div>
|
||||||
|
<div className="module-name">
|
||||||
|
ProtogenCore
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="module-item">
|
||||||
|
<div className="module-icon"></div>
|
||||||
|
<div className="module-name">NeuralNet</div>
|
||||||
|
</div>
|
||||||
|
<div className="module-item">
|
||||||
|
<div className="module-icon"></div>
|
||||||
|
<div className="module-name">
|
||||||
|
VisorDisplay
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="module-item">
|
||||||
|
<div className="module-icon"></div>
|
||||||
|
<div className="module-name">FoxTraits</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fox Protocol */}
|
||||||
|
<div
|
||||||
|
className={`boot-stage fox-protocol ${bootStage === 5 ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="fox-header">
|
||||||
|
ACTIVATING FOX PROTOCOLS
|
||||||
|
</div>
|
||||||
|
<div className="fox-trait">
|
||||||
|
Fluffy tail module: Online
|
||||||
|
</div>
|
||||||
|
<div className="fox-trait">
|
||||||
|
Fox ears: Calibrated
|
||||||
|
</div>
|
||||||
|
<div className="fox-trait">
|
||||||
|
Cuteness factor: Nonexistent
|
||||||
|
</div>
|
||||||
|
<div className="fox-trait">
|
||||||
|
Mischief subroutines: Loaded
|
||||||
|
</div>
|
||||||
|
<div className="fox-trait">
|
||||||
|
ProtoFox integration: Complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logo Display */}
|
||||||
|
<div
|
||||||
|
className={`boot-stage logo-display ${bootStage === 6 && showLogo ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="endos-logo">
|
||||||
|
<span className="logo-end">End</span>
|
||||||
|
<span className="logo-os">OS</span>
|
||||||
|
</div>
|
||||||
|
<div className="logo-subtitle">
|
||||||
|
ProtoFox Operating System
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* System Ready */}
|
||||||
|
<div
|
||||||
|
className={`boot-stage system-ready ${bootStage === 7 && bootComplete ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="ready-status">SYSTEM ACTIVATED</div>
|
||||||
|
<div className="welcome-message">
|
||||||
|
Welcome back, ProtoFox
|
||||||
|
</div>
|
||||||
|
<div className="boot-complete-message">
|
||||||
|
EndOS v1.0 is fully operational
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="module-item">
|
|
||||||
<div className="module-icon"></div>
|
|
||||||
<div className="module-name">NeuralNet</div>
|
|
||||||
</div>
|
|
||||||
<div className="module-item">
|
|
||||||
<div className="module-icon"></div>
|
|
||||||
<div className="module-name">VisorDisplay</div>
|
|
||||||
</div>
|
|
||||||
<div className="module-item">
|
|
||||||
<div className="module-icon"></div>
|
|
||||||
<div className="module-name">FoxTraits</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fox Protocol */}
|
{/* Skip button - More prominent and always visible */}
|
||||||
<div className={`boot-stage fox-protocol ${bootStage === 5 ? 'active' : ''}`}>
|
<button
|
||||||
<div className="fox-header">ACTIVATING FOX PROTOCOLS</div>
|
className="skip-button"
|
||||||
<div className="fox-trait">Fluffy tail module: Online</div>
|
onClick={handleSkip}
|
||||||
<div className="fox-trait">Fox ears: Calibrated</div>
|
aria-label="Skip boot animation"
|
||||||
<div className="fox-trait">Cuteness factor: Nonexistent</div>
|
>
|
||||||
<div className="fox-trait">Mischief subroutines: Loaded</div>
|
SKIP BOOT SEQUENCE
|
||||||
<div className="fox-trait">ProtoFox integration: Complete</div>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Logo Display */}
|
|
||||||
<div className={`boot-stage logo-display ${bootStage === 6 && showLogo ? 'active' : ''}`}>
|
|
||||||
<div className="endos-logo">
|
|
||||||
<span className="logo-end">End</span>
|
|
||||||
<span className="logo-os">OS</span>
|
|
||||||
</div>
|
|
||||||
<div className="logo-subtitle">ProtoFox Operating System</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* System Ready */}
|
|
||||||
<div className={`boot-stage system-ready ${bootStage === 7 && bootComplete ? 'active' : ''}`}>
|
|
||||||
<div className="ready-status">SYSTEM ACTIVATED</div>
|
|
||||||
<div className="welcome-message">Welcome back, ProtoFox</div>
|
|
||||||
<div className="boot-complete-message">EndOS v1.0 is fully operational</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
|
||||||
{/* Skip button - More prominent and always visible */}
|
|
||||||
<button
|
|
||||||
className="skip-button"
|
|
||||||
onClick={handleSkip}
|
|
||||||
aria-label="Skip boot animation"
|
|
||||||
>
|
|
||||||
SKIP BOOT SEQUENCE
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EndOSBootAnimation;
|
export default EndOSBootAnimation;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, ErrorInfo } from 'react';
|
import { Component, ErrorInfo } from "react";
|
||||||
import { ErrorBoundaryProps, ErrorBoundaryState } from '@/types';
|
import { ErrorBoundaryProps, ErrorBoundaryState } from "@/types";
|
||||||
|
|
||||||
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
constructor(props: ErrorBoundaryProps) {
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
|
@ -12,7 +12,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
|
||||||
console.error('Error caught by boundary:', error, errorInfo);
|
console.error("Error caught by boundary:", error, errorInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { FoxCardProps } from '@/types';
|
import type { FoxCardProps } from "@/types";
|
||||||
import '@/styles/FoxCard.css';
|
import "@/styles/FoxCard.css";
|
||||||
|
|
||||||
const FoxCard = ({ children, className = '' }: FoxCardProps) => (
|
const FoxCard = ({ children, className = "" }: FoxCardProps) => (
|
||||||
<div className={`fox-card ${className}`.trim()}>
|
<div className={`fox-card ${className}`.trim()}>
|
||||||
<div className="fox-ear fox-ear-left" />
|
<div className="fox-ear fox-ear-left" />
|
||||||
<div className="fox-ear fox-ear-right" />
|
<div className="fox-ear fox-ear-right" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { GithubRepo } from '@/types';
|
import { GithubRepo } from "@/types";
|
||||||
import '@/styles/GithubRepos.css';
|
import "@/styles/GithubRepos.css";
|
||||||
|
|
||||||
interface GithubReposProps {
|
interface GithubReposProps {
|
||||||
repos: GithubRepo[];
|
repos: GithubRepo[];
|
||||||
|
|
@ -11,14 +11,21 @@ const GithubRepos: React.FC<GithubReposProps> = ({ repos }) => {
|
||||||
<div className="repos-grid">
|
<div className="repos-grid">
|
||||||
{repos.map((repo) => (
|
{repos.map((repo) => (
|
||||||
<div key={repo.id} className="repo-card">
|
<div key={repo.id} className="repo-card">
|
||||||
<a href={repo.html_url} target="_blank" rel="noopener noreferrer" className="repo-name">
|
<a
|
||||||
|
href={repo.html_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="repo-name"
|
||||||
|
>
|
||||||
{repo.name}
|
{repo.name}
|
||||||
</a>
|
</a>
|
||||||
<p className="repo-description">
|
<p className="repo-description">
|
||||||
{repo.description || 'No description provided.'}
|
{repo.description || "No description provided."}
|
||||||
</p>
|
</p>
|
||||||
{repo.language && (
|
{repo.language && (
|
||||||
<span className="repo-language">{repo.language}</span>
|
<span className="repo-language">
|
||||||
|
{repo.language}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
{repo.languages && repo.languages.length > 0 && (
|
{repo.languages && repo.languages.length > 0 && (
|
||||||
<div className="repo-languages">
|
<div className="repo-languages">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import '@/styles/LoadingFox.css';
|
import "@/styles/LoadingFox.css";
|
||||||
|
|
||||||
const LoadingFox = () => {
|
const LoadingFox = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,161 +1,173 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Music } from 'lucide-react';
|
import { Music } from "lucide-react";
|
||||||
|
|
||||||
interface LastFMImage {
|
interface LastFMImage {
|
||||||
'#text': string;
|
"#text": string;
|
||||||
size: string;
|
size: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LastFMTrack {
|
interface LastFMTrack {
|
||||||
name: string;
|
name: string;
|
||||||
artist: {
|
artist: {
|
||||||
'#text': string;
|
"#text": string;
|
||||||
};
|
};
|
||||||
image: LastFMImage[];
|
image: LastFMImage[];
|
||||||
'@attr'?: {
|
"@attr"?: {
|
||||||
nowplaying: string;
|
nowplaying: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LastFMResponse {
|
interface LastFMResponse {
|
||||||
recenttracks: {
|
recenttracks: {
|
||||||
track: LastFMTrack[];
|
track: LastFMTrack[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CurrentTrack {
|
interface CurrentTrack {
|
||||||
name: string;
|
name: string;
|
||||||
artist: string;
|
artist: string;
|
||||||
image: string;
|
image: string;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MusicDisplay = () => {
|
const MusicDisplay = () => {
|
||||||
const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null);
|
const [currentTrack, setCurrentTrack] = useState<CurrentTrack | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCurrentTrack = async () => {
|
const fetchCurrentTrack = async () => {
|
||||||
try {
|
try {
|
||||||
const API_KEY = import.meta.env.VITE_LASTFM_API_KEY;
|
const API_KEY = import.meta.env.VITE_LASTFM_API_KEY;
|
||||||
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
|
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
|
||||||
|
|
||||||
if (!API_KEY || !USERNAME) {
|
if (!API_KEY || !USERNAME) {
|
||||||
throw new Error('Last.fm API key or username not configured');
|
throw new Error(
|
||||||
}
|
"Last.fm API key or username not configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${USERNAME}&api_key=${API_KEY}&format=json&limit=1`;
|
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${USERNAME}&api_key=${API_KEY}&format=json&limit=1`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: LastFMResponse = await response.json();
|
const data: LastFMResponse = await response.json();
|
||||||
|
|
||||||
if (!data.recenttracks?.track?.length) {
|
|
||||||
setCurrentTrack(null);
|
|
||||||
setExpanded(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const track = data.recenttracks.track[0];
|
if (!data.recenttracks?.track?.length) {
|
||||||
|
setCurrentTrack(null);
|
||||||
const largeImage = track.image.find(img => img.size === 'large');
|
setExpanded(true);
|
||||||
const imageUrl = largeImage ? largeImage['#text'] :
|
return;
|
||||||
track.image[track.image.length - 1] ? track.image[track.image.length - 1]['#text'] :
|
}
|
||||||
'/placeholder-album.jpg';
|
|
||||||
|
|
||||||
const isCurrentlyPlaying = track['@attr']?.nowplaying === 'true';
|
const track = data.recenttracks.track[0];
|
||||||
|
|
||||||
if (!isCurrentlyPlaying) {
|
const largeImage = track.image.find(
|
||||||
setCurrentTrack(null);
|
(img) => img.size === "large",
|
||||||
setExpanded(true);
|
);
|
||||||
return;
|
const imageUrl = largeImage
|
||||||
}
|
? largeImage["#text"]
|
||||||
|
: track.image[track.image.length - 1]
|
||||||
|
? track.image[track.image.length - 1]["#text"]
|
||||||
|
: "/placeholder-album.jpg";
|
||||||
|
|
||||||
setCurrentTrack({
|
const isCurrentlyPlaying =
|
||||||
name: track.name,
|
track["@attr"]?.nowplaying === "true";
|
||||||
artist: track.artist['#text'],
|
|
||||||
image: imageUrl,
|
|
||||||
isPlaying: true
|
|
||||||
});
|
|
||||||
setExpanded(false);
|
|
||||||
setError(null);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'An error occurred';
|
|
||||||
console.error('Last.fm error:', error);
|
|
||||||
setError(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchCurrentTrack();
|
if (!isCurrentlyPlaying) {
|
||||||
const interval = setInterval(fetchCurrentTrack, 30000);
|
setCurrentTrack(null);
|
||||||
return () => clearInterval(interval);
|
setExpanded(true);
|
||||||
}, []);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTrack({
|
||||||
|
name: track.name,
|
||||||
|
artist: track.artist["#text"],
|
||||||
|
image: imageUrl,
|
||||||
|
isPlaying: true,
|
||||||
|
});
|
||||||
|
setExpanded(false);
|
||||||
|
setError(null);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "An error occurred";
|
||||||
|
console.error("Last.fm error:", error);
|
||||||
|
setError(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCurrentTrack();
|
||||||
|
const interval = setInterval(fetchCurrentTrack, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="h-[88px] flex items-center justify-center p-4 animate-pulse">
|
||||||
|
<Music className="w-5 h-5 mr-2" />
|
||||||
|
<span>Loading music...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="h-[88px] flex items-center justify-center p-4 text-red-400">
|
||||||
|
<span>Unable to load music data: {error}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentTrack) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? "h-[352px]" : "h-[88px]"}`}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
title="Spotify Playlist"
|
||||||
|
style={{ borderRadius: "12px" }}
|
||||||
|
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
|
||||||
|
width="100%"
|
||||||
|
height="352"
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[88px] flex items-center justify-center p-4 animate-pulse">
|
<div className="h-[88px] flex items-center gap-4 p-4 bg-gradient-card rounded-lg">
|
||||||
<Music className="w-5 h-5 mr-2" />
|
<div className="relative w-16 h-16 flex-shrink-0">
|
||||||
<span>Loading music...</span>
|
<img
|
||||||
</div>
|
src={currentTrack.image}
|
||||||
);
|
alt={`${currentTrack.name} album art`}
|
||||||
}
|
className="w-full h-full object-cover rounded-md shadow-lg"
|
||||||
|
/>
|
||||||
if (error) {
|
<div className="absolute -top-1 -right-1 w-3 h-3">
|
||||||
return (
|
<span className="absolute w-full h-full bg-accent-neon rounded-full animate-ping"></span>
|
||||||
<div className="h-[88px] flex items-center justify-center p-4 text-red-400">
|
<span className="absolute w-full h-full bg-accent-neon rounded-full"></span>
|
||||||
<span>Unable to load music data: {error}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="flex flex-col min-w-0">
|
||||||
}
|
<span className="font-medium truncate">
|
||||||
|
{currentTrack.name}
|
||||||
if (!currentTrack) {
|
</span>
|
||||||
return (
|
<span className="text-sm opacity-75 truncate">
|
||||||
<div className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? 'h-[352px]' : 'h-[88px]'}`}>
|
{currentTrack.artist}
|
||||||
<iframe
|
</span>
|
||||||
title="Spotify Playlist"
|
</div>
|
||||||
style={{ borderRadius: '12px' }}
|
|
||||||
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
|
|
||||||
width="100%"
|
|
||||||
height="352"
|
|
||||||
frameBorder="0"
|
|
||||||
allowFullScreen
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-[88px] flex items-center gap-4 p-4 bg-gradient-card rounded-lg">
|
|
||||||
<div className="relative w-16 h-16 flex-shrink-0">
|
|
||||||
<img
|
|
||||||
src={currentTrack.image}
|
|
||||||
alt={`${currentTrack.name} album art`}
|
|
||||||
className="w-full h-full object-cover rounded-md shadow-lg"
|
|
||||||
/>
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3">
|
|
||||||
<span className="absolute w-full h-full bg-accent-neon rounded-full animate-ping"></span>
|
|
||||||
<span className="absolute w-full h-full bg-accent-neon rounded-full"></span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="font-medium truncate">
|
|
||||||
{currentTrack.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm opacity-75 truncate">
|
|
||||||
{currentTrack.artist}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MusicDisplay;
|
export default MusicDisplay;
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,57 @@
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { Home, Code, BookOpen, Twitch} from 'lucide-react';
|
import { Home, Code, BookOpen, Twitch } from "lucide-react";
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const handleRedirect = () => {
|
||||||
|
window.open("https://vnc.endoftimee.tech", "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar">
|
<nav className="navbar">
|
||||||
<div className="nav-content">
|
<div className="nav-content">
|
||||||
<div className="nav-links">
|
<div className="nav-links">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
|
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
|
||||||
>
|
>
|
||||||
<Home size={20} />
|
<Home size={20} />
|
||||||
<span>About</span>
|
<span>About</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/projects"
|
to="/projects"
|
||||||
className={`nav-link ${location.pathname === '/projects' ? 'active' : ''}`}
|
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
|
||||||
>
|
>
|
||||||
<Code size={20} />
|
<Code size={20} />
|
||||||
<span>Projects</span>
|
<span>Projects</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
{/*
|
||||||
<Link
|
<Link
|
||||||
to="/apcsp"
|
to="/apcsp"
|
||||||
className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`}
|
className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
<BookOpen size={20} />
|
<BookOpen size={20} />
|
||||||
<span>APCSP</span>
|
<span>APCSP</span>
|
||||||
</Link>
|
</Link> */}
|
||||||
|
|
||||||
<Link
|
<button
|
||||||
to="/novnc"
|
onClick={handleRedirect}
|
||||||
className={`nav-link ${location.pathname === '/novnc' ? 'active' : ''}`}
|
className="nav-link"
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: "0 15px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Code size={20} />
|
<Code size={20} />
|
||||||
<span>noVNC</span>
|
<span>NoVNC</span>
|
||||||
</Link>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="https://twitch.tv/EndofTimee"
|
href="https://twitch.tv/EndofTimee"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
@ -49,14 +60,14 @@ const Navbar = () => {
|
||||||
<Twitch size={20} />
|
<Twitch size={20} />
|
||||||
<span>Stream</span>
|
<span>Stream</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="nav-link">
|
<div className="nav-link">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://github.com/sponsors/EndofTimee/button"
|
src="https://github.com/sponsors/System-End/button"
|
||||||
title="Sponsor EndofTimee"
|
title="Sponsor End!"
|
||||||
height="32"
|
height="32"
|
||||||
width="114"
|
width="114"
|
||||||
style={{ border: 0, borderRadius: '6px' }}
|
style={{ border: 0, borderRadius: "6px" }}
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,4 +76,4 @@ const Navbar = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Navbar;
|
export default Navbar;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
/** not used */
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
const SpotifyEmbed = () => {
|
const SpotifyEmbed = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full aspect-[100/35]">
|
<div className="w-full aspect-[100/35]">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
|
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{ borderRadius: '12px' }}
|
style={{ borderRadius: "12px" }}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SpotifyEmbed;
|
export default SpotifyEmbed;
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,109 @@
|
||||||
.theme-toggle-container {
|
.theme-toggle-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle input {
|
.theme-toggle input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-track {
|
.toggle-track {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-indicator {
|
.toggle-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
background-color: var(--text-primary);
|
background-color: var(--text-primary);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fox ears */
|
/* Fox ears */
|
||||||
.fox-ear {
|
.fox-ear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background-color: var(--fox-orange);
|
background-color: var(--fox-orange);
|
||||||
top: -5px;
|
top: -5px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-ear.left {
|
.fox-ear.left {
|
||||||
left: 3px;
|
left: 3px;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
border-radius: 0 0 0 5px;
|
border-radius: 0 0 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-ear.right {
|
.fox-ear.right {
|
||||||
right: 3px;
|
right: 3px;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
border-radius: 0 0 5px 0;
|
border-radius: 0 0 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-ear.active {
|
.fox-ear.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Visor element */
|
/* Visor element */
|
||||||
.toggle-visor {
|
.toggle-visor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When toggled to protofox mode */
|
/* When toggled to protofox mode */
|
||||||
input:checked + .toggle-track {
|
input:checked + .toggle-track {
|
||||||
background-color: var(--background-primary);
|
background-color: var(--background-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-track .toggle-indicator {
|
input:checked + .toggle-track .toggle-indicator {
|
||||||
left: 32px;
|
left: 32px;
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-track .toggle-visor {
|
input:checked + .toggle-track .toggle-visor {
|
||||||
background-color: #00E5FF;
|
background-color: #00e5ff;
|
||||||
box-shadow: 0 0 5px #00E5FF;
|
box-shadow: 0 0 5px #00e5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-label {
|
.toggle-label {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-track + .toggle-label {
|
input:checked + .toggle-track + .toggle-label {
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,52 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import './ThemeToggle.css';
|
import "./ThemeToggle.css";
|
||||||
|
|
||||||
interface ThemeToggleProps {
|
interface ThemeToggleProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => {
|
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = "" }) => {
|
||||||
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
|
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
|
||||||
// Check if user had previously selected protofox mode
|
// Check if user had previously selected protofox mode
|
||||||
return localStorage.getItem('theme') === 'protofox';
|
return localStorage.getItem("theme") === "protofox";
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply theme class to body when toggle changes
|
// Apply theme class to body when toggle changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isProtoFoxMode) {
|
if (isProtoFoxMode) {
|
||||||
document.body.classList.add('protofox-theme');
|
document.body.classList.add("protofox-theme");
|
||||||
localStorage.setItem('theme', 'protofox');
|
localStorage.setItem("theme", "protofox");
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('protofox-theme');
|
document.body.classList.remove("protofox-theme");
|
||||||
localStorage.setItem('theme', 'default');
|
localStorage.setItem("theme", "default");
|
||||||
}
|
}
|
||||||
}, [isProtoFoxMode]);
|
}, [isProtoFoxMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`theme-toggle-container ${className}`}>
|
<div className={`theme-toggle-container ${className}`}>
|
||||||
<label className="theme-toggle">
|
<label className="theme-toggle">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isProtoFoxMode}
|
checked={isProtoFoxMode}
|
||||||
onChange={() => setIsProtoFoxMode(!isProtoFoxMode)}
|
onChange={() => setIsProtoFoxMode(!isProtoFoxMode)}
|
||||||
/>
|
/>
|
||||||
<div className="toggle-track">
|
<div className="toggle-track">
|
||||||
<div className="toggle-indicator">
|
<div className="toggle-indicator">
|
||||||
<div className={`fox-ear left ${isProtoFoxMode ? 'active' : ''}`}></div>
|
<div
|
||||||
<div className={`fox-ear right ${isProtoFoxMode ? 'active' : ''}`}></div>
|
className={`fox-ear left ${isProtoFoxMode ? "active" : ""}`}
|
||||||
<div className="toggle-visor"></div>
|
></div>
|
||||||
</div>
|
<div
|
||||||
|
className={`fox-ear right ${isProtoFoxMode ? "active" : ""}`}
|
||||||
|
></div>
|
||||||
|
<div className="toggle-visor"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="toggle-label">
|
||||||
|
{isProtoFoxMode ? "ProtoFox Mode" : "Standard Mode"}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span className="toggle-label">
|
);
|
||||||
{isProtoFoxMode ? 'ProtoFox Mode' : 'Standard Mode'}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ThemeToggle;
|
export default ThemeToggle;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { type CardProps } from '@/types';
|
import { type CardProps } from "@/types";
|
||||||
|
|
||||||
export const Card: React.FC<CardProps> = ({ className, children }) => (
|
export const Card: React.FC<CardProps> = ({ className, children }) => (
|
||||||
<div className={`card ${className || ''}`}>{children}</div>
|
<div className={`card ${className || ""}`}>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CardHeader: React.FC<CardProps> = ({ className, children }) => (
|
export const CardHeader: React.FC<CardProps> = ({ className, children }) => (
|
||||||
<div className={`card-header ${className || ''}`}>{children}</div>
|
<div className={`card-header ${className || ""}`}>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CardTitle: React.FC<CardProps> = ({ className, children }) => (
|
export const CardTitle: React.FC<CardProps> = ({ className, children }) => (
|
||||||
<h2 className={`card-title ${className || ''}`}>{children}</h2>
|
<h2 className={`card-title ${className || ""}`}>{children}</h2>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const CardContent: React.FC<CardProps> = ({ className, children }) => (
|
export const CardContent: React.FC<CardProps> = ({ className, children }) => (
|
||||||
<div className={`card-content ${className || ''}`}>{children}</div>
|
<div className={`card-content ${className || ""}`}>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,163 +1,177 @@
|
||||||
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
interface SystemMember {
|
interface SystemMember {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemState {
|
interface SystemState {
|
||||||
safetyLevel: 'safe' | 'unsafe' | 'sorta-safe' | 'unknown';
|
safetyLevel: "safe" | "unsafe" | "sorta-safe" | "unknown";
|
||||||
mentalState: 'ok' | 'bad' | 'very-bad' | 'panic' | 'spiraling' | 'unstable' | 'delusional';
|
mentalState:
|
||||||
frontingStatus: 'single' | 'co-fronting' | 'switching' | 'unknown';
|
| "ok"
|
||||||
currentFronters: SystemMember[];
|
| "bad"
|
||||||
|
| "very-bad"
|
||||||
|
| "panic"
|
||||||
|
| "spiraling"
|
||||||
|
| "unstable"
|
||||||
|
| "delusional";
|
||||||
|
frontingStatus: "single" | "co-fronting" | "switching" | "unknown";
|
||||||
|
currentFronters: SystemMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
systemState: SystemState | null;
|
systemState: SystemState | null;
|
||||||
login: (username: string, password: string) => Promise<boolean>;
|
login: (username: string, password: string) => Promise<boolean>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
updateSystemState: (newState: Partial<SystemState>) => void;
|
updateSystemState: (newState: Partial<SystemState>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// System members data
|
// System members data
|
||||||
const systemMembers: SystemMember[] = [
|
const systemMembers: SystemMember[] = [
|
||||||
{ id: '1', name: 'Aurora', role: 'Host', color: '#9d4edd' },
|
{ id: "1", name: "Aurora", role: "Host", color: "#9d4edd" },
|
||||||
{ id: '2', name: 'Alex', role: 'Younger', color: '#4ea8de' },
|
{ id: "2", name: "Alex", role: "Younger", color: "#4ea8de" },
|
||||||
{ id: '3', name: 'Psy', role: 'Protector', color: '#5e548e' },
|
{ id: "3", name: "Psy", role: "Protector", color: "#5e548e" },
|
||||||
{ id: '4', name: 'Xander', role: 'Caretaker', color: '#219ebc' },
|
{ id: "4", name: "Xander", role: "Caretaker", color: "#219ebc" },
|
||||||
{ id: '5', name: 'Unknown', role: 'Fragment', color: '#6c757d' },
|
{ id: "5", name: "Unknown", role: "Fragment", color: "#6c757d" },
|
||||||
{ id: '6', name: 'The thing', role: 'Persecutor', color: '#e63946' },
|
{ id: "6", name: "The thing", role: "Persecutor", color: "#e63946" },
|
||||||
{ id: '7', name: 'Unknown 2', role: 'Fragment', color: '#6c757d' },
|
{ id: "7", name: "Unknown 2", role: "Fragment", color: "#6c757d" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Creating the context with a default value of null
|
// Creating the context with a default value of null
|
||||||
const AuthContext = createContext<AuthContextType | null>(null);
|
const AuthContext = createContext<AuthContextType | null>(null);
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
// Initialize authentication state from localStorage if available
|
// Initialize authentication state from localStorage if available
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
||||||
const stored = localStorage.getItem('isAuthenticated');
|
const stored = localStorage.getItem("isAuthenticated");
|
||||||
return stored === 'true';
|
return stored === "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
const [username, setUsername] = useState<string | null>(() => {
|
const [username, setUsername] = useState<string | null>(() => {
|
||||||
return localStorage.getItem('username');
|
return localStorage.getItem("username");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize system state from localStorage or set defaults
|
// Initialize system state from localStorage or set defaults
|
||||||
const [systemState, setSystemState] = useState<SystemState | null>(() => {
|
const [systemState, setSystemState] = useState<SystemState | null>(() => {
|
||||||
const stored = localStorage.getItem('systemState');
|
const stored = localStorage.getItem("systemState");
|
||||||
if (stored && isAuthenticated) {
|
if (stored && isAuthenticated) {
|
||||||
return JSON.parse(stored);
|
return JSON.parse(stored);
|
||||||
}
|
}
|
||||||
return isAuthenticated ? {
|
return isAuthenticated
|
||||||
safetyLevel: 'safe',
|
? {
|
||||||
mentalState: 'ok',
|
safetyLevel: "safe",
|
||||||
frontingStatus: 'single',
|
mentalState: "ok",
|
||||||
currentFronters: [systemMembers[0]] // Default to Aurora as fronter
|
frontingStatus: "single",
|
||||||
} : null;
|
currentFronters: [systemMembers[0]], // Default to Aurora as fronter
|
||||||
});
|
}
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
|
||||||
// Update localStorage when auth state changes
|
// Update localStorage when auth state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('isAuthenticated', isAuthenticated.toString());
|
localStorage.setItem("isAuthenticated", isAuthenticated.toString());
|
||||||
if (username) {
|
if (username) {
|
||||||
localStorage.setItem('username', username);
|
localStorage.setItem("username", username);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('username');
|
localStorage.removeItem("username");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If logged out, clear system state
|
// If logged out, clear system state
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
localStorage.removeItem('systemState');
|
localStorage.removeItem("systemState");
|
||||||
setSystemState(null);
|
setSystemState(null);
|
||||||
} else if (systemState) {
|
} else if (systemState) {
|
||||||
localStorage.setItem('systemState', JSON.stringify(systemState));
|
localStorage.setItem("systemState", JSON.stringify(systemState));
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, username, systemState]);
|
}, [isAuthenticated, username, systemState]);
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string) => {
|
||||||
// For security, add a slight delay to prevent rapid brute force attempts
|
// For security, add a slight delay to prevent rapid brute force attempts
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
|
||||||
// We use credential verification with multiple allowed passwords for different contexts
|
// We use credential verification with multiple allowed passwords for different contexts
|
||||||
const validCredentials = [
|
const validCredentials = [{ user: "system", pass: "." }];
|
||||||
{ user: 'system', pass: '.' },
|
|
||||||
];
|
const isValid = validCredentials.some(
|
||||||
|
(cred) =>
|
||||||
const isValid = validCredentials.some(
|
cred.user === username.toLowerCase() && cred.pass === password,
|
||||||
cred => cred.user === username.toLowerCase() && cred.pass === password
|
);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
setUsername(username);
|
||||||
|
|
||||||
|
// Initialize system state on login
|
||||||
|
const initialState: SystemState = {
|
||||||
|
safetyLevel: "safe",
|
||||||
|
mentalState: "ok",
|
||||||
|
frontingStatus: "single",
|
||||||
|
currentFronters: [systemMembers[0]],
|
||||||
|
};
|
||||||
|
|
||||||
|
setSystemState(initialState);
|
||||||
|
localStorage.setItem("systemState", JSON.stringify(initialState));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
// Add a short delay for better UX
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUsername(null);
|
||||||
|
setSystemState(null);
|
||||||
|
|
||||||
|
// Clear sensitive data from localStorage
|
||||||
|
localStorage.removeItem("systemState");
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSystemState = (newState: Partial<SystemState>) => {
|
||||||
|
if (!systemState) return;
|
||||||
|
|
||||||
|
const updatedState = { ...systemState, ...newState };
|
||||||
|
setSystemState(updatedState);
|
||||||
|
localStorage.setItem("systemState", JSON.stringify(updatedState));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct the context value
|
||||||
|
const contextValue: AuthContextType = {
|
||||||
|
isAuthenticated,
|
||||||
|
username,
|
||||||
|
systemState,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
updateSystemState,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
setUsername(username);
|
|
||||||
|
|
||||||
// Initialize system state on login
|
|
||||||
const initialState: SystemState = {
|
|
||||||
safetyLevel: 'safe',
|
|
||||||
mentalState: 'ok',
|
|
||||||
frontingStatus: 'single',
|
|
||||||
currentFronters: [systemMembers[0]]
|
|
||||||
};
|
|
||||||
|
|
||||||
setSystemState(initialState);
|
|
||||||
localStorage.setItem('systemState', JSON.stringify(initialState));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
// Add a short delay for better UX
|
|
||||||
setTimeout(() => {
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
setUsername(null);
|
|
||||||
setSystemState(null);
|
|
||||||
|
|
||||||
// Clear sensitive data from localStorage
|
|
||||||
localStorage.removeItem('systemState');
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSystemState = (newState: Partial<SystemState>) => {
|
|
||||||
if (!systemState) return;
|
|
||||||
|
|
||||||
const updatedState = { ...systemState, ...newState };
|
|
||||||
setSystemState(updatedState);
|
|
||||||
localStorage.setItem('systemState', JSON.stringify(updatedState));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Construct the context value
|
|
||||||
const contextValue: AuthContextType = {
|
|
||||||
isAuthenticated,
|
|
||||||
username,
|
|
||||||
systemState,
|
|
||||||
login,
|
|
||||||
logout,
|
|
||||||
updateSystemState
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom hook for easier context consumption
|
// Custom hook for easier context consumption
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export system members data for use in other components
|
// Export system members data for use in other components
|
||||||
|
|
|
||||||
10
src/env.d.ts
vendored
10
src/env.d.ts
vendored
|
|
@ -1,12 +1,12 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_SPOTIFY_CLIENT_ID: string
|
readonly VITE_SPOTIFY_CLIENT_ID: string;
|
||||||
readonly VITE_SPOTIFY_CLIENT_SECRET: string
|
readonly VITE_SPOTIFY_CLIENT_SECRET: string;
|
||||||
readonly VITE_SPOTIFY_REDIRECT_URI: string
|
readonly VITE_SPOTIFY_REDIRECT_URI: string;
|
||||||
readonly VITE_WORKER_URL: string
|
readonly VITE_WORKER_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,107 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
import { useGameLoop } from '../hooks/useGameLoop';
|
import { useGameLoop } from "../hooks/useGameLoop";
|
||||||
import { useGameControls } from '../hooks/useGameControls';
|
import { useGameControls } from "../hooks/useGameControls";
|
||||||
import { Player } from './Player';
|
import { Player } from "./Player";
|
||||||
import { GameHUD } from './GameHUD';
|
import { GameHUD } from "./GameHUD";
|
||||||
import { GameOverlay } from './GameOverlay';
|
import { GameOverlay } from "./GameOverlay";
|
||||||
|
|
||||||
const FoxGame: React.FC = () => {
|
const FoxGame: React.FC = () => {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
const gameStore = useGameStore();
|
const gameStore = useGameStore();
|
||||||
|
|
||||||
// Initialize game systems
|
|
||||||
useGameLoop();
|
|
||||||
useGameControls();
|
|
||||||
|
|
||||||
// Konami code activation
|
// Initialize game systems
|
||||||
useEffect(() => {
|
useGameLoop();
|
||||||
const konamiCode = [
|
useGameControls();
|
||||||
'ArrowUp', 'ArrowUp',
|
|
||||||
'ArrowDown', 'ArrowDown',
|
|
||||||
'ArrowLeft', 'ArrowRight',
|
|
||||||
'ArrowLeft', 'ArrowRight',
|
|
||||||
'b', 'a'
|
|
||||||
];
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
// Konami code activation
|
||||||
if (event.key === konamiCode[index]) {
|
useEffect(() => {
|
||||||
index++;
|
const konamiCode = [
|
||||||
if (index === konamiCode.length) {
|
"ArrowUp",
|
||||||
setIsActive(true);
|
"ArrowUp",
|
||||||
gameStore.startNewGame();
|
"ArrowDown",
|
||||||
}
|
"ArrowDown",
|
||||||
} else {
|
"ArrowLeft",
|
||||||
index = 0;
|
"ArrowRight",
|
||||||
}
|
"ArrowLeft",
|
||||||
};
|
"ArrowRight",
|
||||||
|
"b",
|
||||||
|
"a",
|
||||||
|
];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeydown);
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
return () => window.removeEventListener('keydown', handleKeydown);
|
if (event.key === konamiCode[index]) {
|
||||||
}, []);
|
index++;
|
||||||
|
if (index === konamiCode.length) {
|
||||||
|
setIsActive(true);
|
||||||
|
gameStore.startNewGame();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!isActive) return null;
|
window.addEventListener("keydown", handleKeydown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeydown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
if (!isActive) return null;
|
||||||
<div className="fixed inset-0 bg-gradient-to-b from-background-primary to-background-secondary z-50">
|
|
||||||
<div className="relative w-full h-full overflow-hidden game-viewport">
|
return (
|
||||||
{/* Game world */}
|
<div className="fixed inset-0 bg-gradient-to-b from-background-primary to-background-secondary z-50">
|
||||||
<div className="absolute inset-0">
|
<div className="relative w-full h-full overflow-hidden game-viewport">
|
||||||
<Player />
|
{/* Game world */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
{/* Render collectibles */}
|
<Player />
|
||||||
{gameStore.collectibles.map(collectible => (
|
|
||||||
<div
|
{/* Render collectibles */}
|
||||||
key={collectible.id}
|
{gameStore.collectibles.map((collectible) => (
|
||||||
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
|
<div
|
||||||
collectible.type === 'GEM' ? 'bg-purple-500' : 'bg-yellow-400'
|
key={collectible.id}
|
||||||
}`}
|
className={`absolute w-4 h-4 transform -translate-x-1/2 -translate-y-1/2 rounded-full animate-pulse ${
|
||||||
style={{
|
collectible.type === "GEM"
|
||||||
left: `${collectible.position.x}%`,
|
? "bg-purple-500"
|
||||||
top: `${collectible.position.y}%`
|
: "bg-yellow-400"
|
||||||
}}
|
}`}
|
||||||
/>
|
style={{
|
||||||
))}
|
left: `${collectible.position.x}%`,
|
||||||
|
top: `${collectible.position.y}%`,
|
||||||
{/* Render enemies */}
|
}}
|
||||||
{gameStore.enemies.map(enemy => (
|
/>
|
||||||
<div
|
))}
|
||||||
key={enemy.id}
|
|
||||||
className="absolute w-6 h-6 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2"
|
{/* Render enemies */}
|
||||||
style={{
|
{gameStore.enemies.map((enemy) => (
|
||||||
left: `${enemy.position.x}%`,
|
<div
|
||||||
top: `${enemy.position.y}%`
|
key={enemy.id}
|
||||||
}}
|
className="absolute w-6 h-6 bg-red-500 rounded-full transform -translate-x-1/2 -translate-y-1/2"
|
||||||
/>
|
style={{
|
||||||
))}
|
left: `${enemy.position.x}%`,
|
||||||
|
top: `${enemy.position.y}%`,
|
||||||
{/* Render power-ups */}
|
}}
|
||||||
{gameStore.powerUps.map(powerUp => (
|
/>
|
||||||
<div
|
))}
|
||||||
key={powerUp.id}
|
|
||||||
className="absolute w-8 h-8 bg-accent-neon rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-float"
|
{/* Render power-ups */}
|
||||||
style={{
|
{gameStore.powerUps.map((powerUp) => (
|
||||||
left: `${powerUp.position.x}%`,
|
<div
|
||||||
top: `${powerUp.position.y}%`
|
key={powerUp.id}
|
||||||
}}
|
className="absolute w-8 h-8 bg-accent-neon rounded-full transform -translate-x-1/2 -translate-y-1/2 animate-float"
|
||||||
/>
|
style={{
|
||||||
))}
|
left: `${powerUp.position.x}%`,
|
||||||
|
top: `${powerUp.position.y}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HUD and Overlay */}
|
||||||
|
<GameHUD />
|
||||||
|
<GameOverlay />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* HUD and Overlay */}
|
|
||||||
<GameHUD />
|
|
||||||
<GameOverlay />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FoxGame;
|
export default FoxGame;
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,83 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
import { Heart, Star, Timer, Trophy } from 'lucide-react';
|
import { Heart, Star, Timer, Trophy } from "lucide-react";
|
||||||
|
|
||||||
export const GameHUD: React.FC = () => {
|
export const GameHUD: React.FC = () => {
|
||||||
const { player, score, level, timePlayed } = useGameStore();
|
const { player, score, level, timePlayed } = useGameStore();
|
||||||
|
|
||||||
const formatTime = (ms: number) => {
|
const formatTime = (ms: number) => {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
return `${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
|
return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
|
<div className="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none">
|
||||||
{/* Left section - Health and PowerUps */}
|
{/* Left section - Health and PowerUps */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Health bar */}
|
{/* Health bar */}
|
||||||
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
|
<div className="flex items-center gap-2 bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm">
|
||||||
<Heart
|
<Heart
|
||||||
className={`w-6 h-6 ${
|
className={`w-6 h-6 ${
|
||||||
player.health > 20 ? 'text-red-500' : 'text-red-500 animate-pulse'
|
player.health > 20
|
||||||
}`}
|
? "text-red-500"
|
||||||
/>
|
: "text-red-500 animate-pulse"
|
||||||
<div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden">
|
}`}
|
||||||
<div
|
/>
|
||||||
className="h-full bg-red-500 transition-all duration-300"
|
<div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden">
|
||||||
style={{ width: `${player.health}%` }}
|
<div
|
||||||
/>
|
className="h-full bg-red-500 transition-all duration-300"
|
||||||
</div>
|
style={{ width: `${player.health}%` }}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Active power-ups */}
|
{/* Active power-ups */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{player.powerUps.map((powerUp) => (
|
{player.powerUps.map((powerUp) => (
|
||||||
<div
|
<div
|
||||||
key={powerUp.id}
|
key={powerUp.id}
|
||||||
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
|
className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div className="w-8 h-8 relative">
|
<div className="w-8 h-8 relative">
|
||||||
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
|
<div className="absolute inset-0 bg-accent-primary/20 rounded-full animate-ping" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Center - Score and Level */}
|
{/* Center - Score and Level */}
|
||||||
<div className="absolute left-1/2 top-0 -translate-x-1/2 text-center space-y-2">
|
<div className="absolute left-1/2 top-0 -translate-x-1/2 text-center space-y-2">
|
||||||
<div className="bg-background-primary/50 px-4 py-2 rounded-lg backdrop-blur-sm">
|
<div className="bg-background-primary/50 px-4 py-2 rounded-lg backdrop-blur-sm">
|
||||||
<div className="text-2xl font-bold text-accent-neon">Level {level}</div>
|
<div className="text-2xl font-bold text-accent-neon">
|
||||||
<div className="flex items-center justify-center gap-2">
|
Level {level}
|
||||||
<Star className="w-5 h-5 text-yellow-400" />
|
</div>
|
||||||
<span className="text-xl">{score.toLocaleString()}</span>
|
<div className="flex items-center justify-center gap-2">
|
||||||
</div>
|
<Star className="w-5 h-5 text-yellow-400" />
|
||||||
</div>
|
<span className="text-xl">
|
||||||
</div>
|
{score.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Right section - Time and High Score */}
|
{/* Right section - Time and High Score */}
|
||||||
<div className="space-y-4 text-right">
|
<div className="space-y-4 text-right">
|
||||||
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
||||||
<Timer className="w-5 h-5 text-accent-primary" />
|
<Timer className="w-5 h-5 text-accent-primary" />
|
||||||
<span>{formatTime(timePlayed)}</span>
|
<span>{formatTime(timePlayed)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
||||||
|
<Trophy className="w-5 h-5 text-yellow-400" />
|
||||||
|
<span>
|
||||||
|
Best:{" "}
|
||||||
|
{Math.max(
|
||||||
|
...useGameStore.getState().highScores,
|
||||||
|
).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-background-primary/50 p-2 rounded-lg backdrop-blur-sm flex items-center gap-2">
|
);
|
||||||
<Trophy className="w-5 h-5 text-yellow-400" />
|
};
|
||||||
<span>Best: {Math.max(...useGameStore.getState().highScores).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,89 @@
|
||||||
// src/games/fox-adventure/components/GameOverlay.tsx
|
// src/games/fox-adventure/components/GameOverlay.tsx
|
||||||
import { Play, RotateCcw } from 'lucide-react';
|
import { Play, RotateCcw } from "lucide-react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
|
|
||||||
export const GameOverlay: React.FC = () => {
|
export const GameOverlay: React.FC = () => {
|
||||||
const { gameStatus, score, startNewGame, resumeGame } = useGameStore();
|
const { gameStatus, score, startNewGame, resumeGame } = useGameStore();
|
||||||
|
|
||||||
if (gameStatus === 'PLAYING') return null;
|
if (gameStatus === "PLAYING") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 bg-background-primary/80 backdrop-blur-md flex items-center justify-center">
|
<div className="absolute inset-0 bg-background-primary/80 backdrop-blur-md flex items-center justify-center">
|
||||||
<div className="max-w-md w-full p-8 bg-gradient-card rounded-xl border border-accent-primary/20">
|
<div className="max-w-md w-full p-8 bg-gradient-card rounded-xl border border-accent-primary/20">
|
||||||
{gameStatus === 'MENU' && (
|
{gameStatus === "MENU" && (
|
||||||
<div className="text-center space-y-6">
|
<div className="text-center space-y-6">
|
||||||
<h1 className="text-4xl font-bold text-glow">Fox Adventure</h1>
|
<h1 className="text-4xl font-bold text-glow">
|
||||||
<p className="text-lg text-text-primary/80">Help the fox collect treasures while avoiding enemies!</p>
|
Fox Adventure
|
||||||
<div className="space-y-4">
|
</h1>
|
||||||
<h2 className="text-xl font-semibold">Controls:</h2>
|
<p className="text-lg text-text-primary/80">
|
||||||
<ul className="text-left space-y-2">
|
Help the fox collect treasures while avoiding
|
||||||
<li>Move: Arrow keys or WASD</li>
|
enemies!
|
||||||
<li>Pause: ESC</li>
|
</p>
|
||||||
<li>Collect items to score points</li>
|
<div className="space-y-4">
|
||||||
<li>Find power-ups to gain advantages</li>
|
<h2 className="text-xl font-semibold">Controls:</h2>
|
||||||
<li>Avoid enemies or find shields</li>
|
<ul className="text-left space-y-2">
|
||||||
</ul>
|
<li>Move: Arrow keys or WASD</li>
|
||||||
|
<li>Pause: ESC</li>
|
||||||
|
<li>Collect items to score points</li>
|
||||||
|
<li>Find power-ups to gain advantages</li>
|
||||||
|
<li>Avoid enemies or find shields</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={startNewGame}
|
||||||
|
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
||||||
|
>
|
||||||
|
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
||||||
|
Start Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gameStatus === "PAUSED" && (
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold text-glow">
|
||||||
|
Game Paused
|
||||||
|
</h2>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={resumeGame}
|
||||||
|
className="px-6 py-3 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center gap-2 group"
|
||||||
|
>
|
||||||
|
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
||||||
|
Resume
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={startNewGame}
|
||||||
|
className="px-6 py-3 bg-background-secondary hover:bg-background-secondary/80 transition-all rounded-lg flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-5 h-5" />
|
||||||
|
Restart
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gameStatus === "GAME_OVER" && (
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<h2 className="text-3xl font-bold text-red-500">
|
||||||
|
Game Over
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xl">Final Score:</p>
|
||||||
|
<p className="text-4xl font-bold text-accent-neon">
|
||||||
|
{score.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={startNewGame}
|
||||||
|
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-5 h-5 group-hover:animate-pulse" />
|
||||||
|
Play Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
</div>
|
||||||
onClick={startNewGame}
|
);
|
||||||
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
};
|
||||||
>
|
|
||||||
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
|
||||||
Start Game
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gameStatus === 'PAUSED' && (
|
|
||||||
<div className="text-center space-y-6">
|
|
||||||
<h2 className="text-3xl font-bold text-glow">Game Paused</h2>
|
|
||||||
<div className="flex justify-center gap-4">
|
|
||||||
<button
|
|
||||||
onClick={resumeGame}
|
|
||||||
className="px-6 py-3 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center gap-2 group"
|
|
||||||
>
|
|
||||||
<Play className="w-5 h-5 group-hover:animate-pulse" />
|
|
||||||
Resume
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={startNewGame}
|
|
||||||
className="px-6 py-3 bg-background-secondary hover:bg-background-secondary/80 transition-all rounded-lg flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-5 h-5" />
|
|
||||||
Restart
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gameStatus === 'GAME_OVER' && (
|
|
||||||
<div className="text-center space-y-6">
|
|
||||||
<h2 className="text-3xl font-bold text-red-500">Game Over</h2>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xl">Final Score:</p>
|
|
||||||
<p className="text-4xl font-bold text-accent-neon">{score.toLocaleString()}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={startNewGame}
|
|
||||||
className="px-8 py-4 bg-accent-primary hover:bg-accent-neon transition-all rounded-lg flex items-center justify-center gap-2 mx-auto group"
|
|
||||||
>
|
|
||||||
<RotateCcw className="w-5 h-5 group-hover:animate-pulse" />
|
|
||||||
Play Again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,64 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
|
|
||||||
export const Player: React.FC = () => {
|
export const Player: React.FC = () => {
|
||||||
const player = useGameStore(state => state.player);
|
const player = useGameStore((state) => state.player);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`absolute transition-all duration-100 ${
|
className={`absolute transition-all duration-100 ${
|
||||||
player.isInvincible ? 'animate-pulse' : ''
|
player.isInvincible ? "animate-pulse" : ""
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${player.position.x}%`,
|
left: `${player.position.x}%`,
|
||||||
top: `${player.position.y}%`,
|
top: `${player.position.y}%`,
|
||||||
transform: 'translate(-50%, -50%)'
|
transform: "translate(-50%, -50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Fox body */}
|
{/* Fox body */}
|
||||||
<div className="relative w-16 h-16">
|
<div className="relative w-16 h-16">
|
||||||
{/* Main body */}
|
{/* Main body */}
|
||||||
<div className="absolute inset-0 bg-fox-orange rounded-full">
|
<div className="absolute inset-0 bg-fox-orange rounded-full">
|
||||||
{/* Face */}
|
{/* Face */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
{/* Eyes */}
|
{/* Eyes */}
|
||||||
<div className="absolute top-1/3 left-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
<div className="absolute top-1/3 left-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
||||||
<div className="absolute top-1/3 right-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
<div className="absolute top-1/3 right-1/4 w-2 h-2 bg-dark-accent rounded-full" />
|
||||||
|
|
||||||
{/* Nose */}
|
|
||||||
<div className="absolute top-1/2 left-1/2 w-2 h-2 bg-dark-accent rounded-full transform -translate-x-1/2" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Ears */}
|
{/* Nose */}
|
||||||
<div className="absolute -top-4 -left-2 w-4 h-4 bg-fox-orange transform rotate-45" />
|
<div className="absolute top-1/2 left-1/2 w-2 h-2 bg-dark-accent rounded-full transform -translate-x-1/2" />
|
||||||
<div className="absolute -top-4 -right-2 w-4 h-4 bg-fox-orange transform -rotate-45" />
|
</div>
|
||||||
|
|
||||||
{/* Tail */}
|
{/* Ears */}
|
||||||
<div className="absolute -bottom-4 left-1/2 w-3 h-6 bg-fox-orange rounded-full transform -translate-x-1/2 origin-top animate-wag" />
|
<div className="absolute -top-4 -left-2 w-4 h-4 bg-fox-orange transform rotate-45" />
|
||||||
|
<div className="absolute -top-4 -right-2 w-4 h-4 bg-fox-orange transform -rotate-45" />
|
||||||
|
|
||||||
|
{/* Tail */}
|
||||||
|
<div className="absolute -bottom-4 left-1/2 w-3 h-6 bg-fox-orange rounded-full transform -translate-x-1/2 origin-top animate-wag" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Power-up effects */}
|
||||||
|
{player.isInvincible && (
|
||||||
|
<div className="absolute inset-0 rounded-full border-4 border-accent-neon animate-pulse" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Key indicator */}
|
||||||
|
{player.hasKey && (
|
||||||
|
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
|
||||||
|
<div className="w-4 h-4 bg-accent-primary rounded-full animate-float" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Health indicator */}
|
||||||
|
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-12">
|
||||||
|
<div className="w-full h-1 bg-background-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-red-500 transition-all duration-300"
|
||||||
|
style={{ width: `${player.health}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* Power-up effects */}
|
};
|
||||||
{player.isInvincible && (
|
|
||||||
<div className="absolute inset-0 rounded-full border-4 border-accent-neon animate-pulse" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Key indicator */}
|
|
||||||
{player.hasKey && (
|
|
||||||
<div className="absolute -top-6 left-1/2 transform -translate-x-1/2">
|
|
||||||
<div className="w-4 h-4 bg-accent-primary rounded-full animate-float" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Health indicator */}
|
|
||||||
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 w-12">
|
|
||||||
<div className="w-full h-1 bg-background-secondary rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-red-500 transition-all duration-300"
|
|
||||||
style={{ width: `${player.health}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,64 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
|
|
||||||
export const useGameControls = () => {
|
export const useGameControls = () => {
|
||||||
const gameStore = useGameStore();
|
const gameStore = useGameStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keys = new Set<string>();
|
const keys = new Set<string>();
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
keys.add(e.key);
|
keys.add(e.key);
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
if (gameStore.gameStatus === 'PLAYING') {
|
|
||||||
gameStore.pauseGame();
|
|
||||||
} else if (gameStore.gameStatus === 'PAUSED') {
|
|
||||||
gameStore.resumeGame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
if (e.key === "Escape") {
|
||||||
keys.delete(e.key);
|
if (gameStore.gameStatus === "PLAYING") {
|
||||||
};
|
gameStore.pauseGame();
|
||||||
|
} else if (gameStore.gameStatus === "PAUSED") {
|
||||||
|
gameStore.resumeGame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updatePlayerMovement = () => {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
if (gameStore.gameStatus !== 'PLAYING') return;
|
keys.delete(e.key);
|
||||||
|
};
|
||||||
|
|
||||||
const direction = { x: 0, y: 0 };
|
const updatePlayerMovement = () => {
|
||||||
|
if (gameStore.gameStatus !== "PLAYING") return;
|
||||||
|
|
||||||
if (keys.has('ArrowUp') || keys.has('w')) direction.y -= 1;
|
const direction = { x: 0, y: 0 };
|
||||||
if (keys.has('ArrowDown') || keys.has('s')) direction.y += 1;
|
|
||||||
if (keys.has('ArrowLeft') || keys.has('a')) direction.x -= 1;
|
|
||||||
if (keys.has('ArrowRight') || keys.has('d')) direction.x += 1;
|
|
||||||
|
|
||||||
if (direction.x !== 0 || direction.y !== 0) {
|
if (keys.has("ArrowUp") || keys.has("w")) direction.y -= 1;
|
||||||
// Normalize diagonal movement
|
if (keys.has("ArrowDown") || keys.has("s")) direction.y += 1;
|
||||||
const magnitude = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
|
if (keys.has("ArrowLeft") || keys.has("a")) direction.x -= 1;
|
||||||
direction.x /= magnitude;
|
if (keys.has("ArrowRight") || keys.has("d")) direction.x += 1;
|
||||||
direction.y /= magnitude;
|
|
||||||
|
|
||||||
gameStore.movePlayer(direction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let animationFrameId: number;
|
if (direction.x !== 0 || direction.y !== 0) {
|
||||||
const gameLoop = () => {
|
// Normalize diagonal movement
|
||||||
updatePlayerMovement();
|
const magnitude = Math.sqrt(
|
||||||
animationFrameId = requestAnimationFrame(gameLoop);
|
direction.x * direction.x + direction.y * direction.y,
|
||||||
};
|
);
|
||||||
animationFrameId = requestAnimationFrame(gameLoop);
|
direction.x /= magnitude;
|
||||||
|
direction.y /= magnitude;
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
gameStore.movePlayer(direction);
|
||||||
window.addEventListener('keyup', handleKeyUp);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
let animationFrameId: number;
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
const gameLoop = () => {
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
updatePlayerMovement();
|
||||||
cancelAnimationFrame(animationFrameId);
|
animationFrameId = requestAnimationFrame(gameLoop);
|
||||||
};
|
};
|
||||||
}, []);
|
animationFrameId = requestAnimationFrame(gameLoop);
|
||||||
};
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
window.addEventListener("keyup", handleKeyUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,78 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import useGameStore from '../state/gameStore';
|
import useGameStore from "../state/gameStore";
|
||||||
|
|
||||||
export const useGameLoop = () => {
|
export const useGameLoop = () => {
|
||||||
const frameRef = useRef<number>();
|
const frameRef = useRef<number>();
|
||||||
const lastUpdateRef = useRef<number>(0);
|
const lastUpdateRef = useRef<number>(0);
|
||||||
const lastSpawnRef = useRef<number>(0);
|
const lastSpawnRef = useRef<number>(0);
|
||||||
const gameStore = useGameStore();
|
const gameStore = useGameStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const spawnCollectible = () => {
|
const spawnCollectible = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds
|
if (now - lastSpawnRef.current < 2000) return; // Spawn every 2 seconds
|
||||||
|
|
||||||
lastSpawnRef.current = now;
|
|
||||||
const collectible: any = {
|
|
||||||
id: `collectible-${now}`,
|
|
||||||
type: Math.random() > 0.8 ? 'GEM' : 'STAR',
|
|
||||||
value: Math.random() > 0.8 ? 10 : 5,
|
|
||||||
position: {
|
|
||||||
x: Math.random() * 90 + 5,
|
|
||||||
y: Math.random() * 90 + 5
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
gameStore.collectibles.push(collectible);
|
|
||||||
};
|
|
||||||
|
|
||||||
const gameLoop = (timestamp: number) => {
|
lastSpawnRef.current = now;
|
||||||
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
|
const collectible: any = {
|
||||||
const deltaTime = timestamp - lastUpdateRef.current;
|
id: `collectible-${now}`,
|
||||||
|
type: Math.random() > 0.8 ? "GEM" : "STAR",
|
||||||
|
value: Math.random() > 0.8 ? 10 : 5,
|
||||||
|
position: {
|
||||||
|
x: Math.random() * 90 + 5,
|
||||||
|
y: Math.random() * 90 + 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (gameStore.gameStatus === 'PLAYING') {
|
gameStore.collectibles.push(collectible);
|
||||||
// Update entities
|
};
|
||||||
gameStore.updateEnemies(deltaTime);
|
|
||||||
|
|
||||||
// Spawn collectibles
|
|
||||||
spawnCollectible();
|
|
||||||
|
|
||||||
// Check collisions
|
const gameLoop = (timestamp: number) => {
|
||||||
const { player, enemies, collectibles } = gameStore;
|
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
|
||||||
|
const deltaTime = timestamp - lastUpdateRef.current;
|
||||||
// Enemy collisions
|
|
||||||
enemies.forEach(enemy => {
|
|
||||||
const dx = player.position.x - enemy.position.x;
|
|
||||||
const dy = player.position.y - enemy.position.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (distance < 5 && !player.isInvincible) {
|
|
||||||
gameStore.takeDamage(20);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collectible collisions
|
if (gameStore.gameStatus === "PLAYING") {
|
||||||
collectibles.forEach(collectible => {
|
// Update entities
|
||||||
const dx = player.position.x - collectible.position.x;
|
gameStore.updateEnemies(deltaTime);
|
||||||
const dy = player.position.y - collectible.position.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (distance < 5) {
|
|
||||||
gameStore.collectItem(collectible.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lastUpdateRef.current = timestamp;
|
// Spawn collectibles
|
||||||
frameRef.current = requestAnimationFrame(gameLoop);
|
spawnCollectible();
|
||||||
};
|
|
||||||
|
|
||||||
frameRef.current = requestAnimationFrame(gameLoop);
|
// Check collisions
|
||||||
|
const { player, enemies, collectibles } = gameStore;
|
||||||
return () => {
|
|
||||||
if (frameRef.current) {
|
// Enemy collisions
|
||||||
cancelAnimationFrame(frameRef.current);
|
enemies.forEach((enemy) => {
|
||||||
}
|
const dx = player.position.x - enemy.position.x;
|
||||||
};
|
const dy = player.position.y - enemy.position.y;
|
||||||
}, []);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 5 && !player.isInvincible) {
|
||||||
|
gameStore.takeDamage(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collectible collisions
|
||||||
|
collectibles.forEach((collectible) => {
|
||||||
|
const dx = player.position.x - collectible.position.x;
|
||||||
|
const dy = player.position.y - collectible.position.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < 5) {
|
||||||
|
gameStore.collectItem(collectible.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateRef.current = timestamp;
|
||||||
|
frameRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
};
|
||||||
|
|
||||||
|
frameRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (frameRef.current) {
|
||||||
|
cancelAnimationFrame(frameRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,129 +1,144 @@
|
||||||
import { create } from 'zustand';
|
import { create } from "zustand";
|
||||||
import type { GameState, Position } from '@/types/game';
|
import type { GameState, Position } from "@/types/game";
|
||||||
|
|
||||||
const useGameStore = create<GameState>((set, get) => ({
|
const useGameStore = create<GameState>((set, get) => ({
|
||||||
player: {
|
|
||||||
position: { x: 50, y: 50 },
|
|
||||||
health: 100,
|
|
||||||
speed: 5,
|
|
||||||
powerUps: [],
|
|
||||||
isInvincible: false,
|
|
||||||
hasKey: false
|
|
||||||
},
|
|
||||||
enemies: [],
|
|
||||||
collectibles: [],
|
|
||||||
powerUps: [],
|
|
||||||
score: 0,
|
|
||||||
level: 1,
|
|
||||||
gameStatus: 'MENU',
|
|
||||||
highScores: [],
|
|
||||||
timePlayed: 0,
|
|
||||||
|
|
||||||
movePlayer: (direction: Position) => {
|
|
||||||
const { player } = get();
|
|
||||||
set({
|
|
||||||
player: {
|
|
||||||
...player,
|
|
||||||
position: {
|
|
||||||
x: Math.max(0, Math.min(100, player.position.x + direction.x * player.speed)),
|
|
||||||
y: Math.max(0, Math.min(100, player.position.y + direction.y * player.speed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateEnemies: () => {
|
|
||||||
const { enemies, player } = get();
|
|
||||||
const updatedEnemies = enemies.map(enemy => {
|
|
||||||
const dx = player.position.x - enemy.position.x;
|
|
||||||
const dy = player.position.y - enemy.position.y;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...enemy,
|
|
||||||
direction: {
|
|
||||||
x: dx / distance,
|
|
||||||
y: dy / distance
|
|
||||||
},
|
|
||||||
position: {
|
|
||||||
x: enemy.position.x + (enemy.direction.x * enemy.speed),
|
|
||||||
y: enemy.position.y + (enemy.direction.y * enemy.speed)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
set({ enemies: updatedEnemies });
|
|
||||||
},
|
|
||||||
|
|
||||||
collectItem: (itemId: string) => {
|
|
||||||
const { collectibles, score } = get();
|
|
||||||
const item = collectibles.find(c => c.id === itemId);
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
set({
|
|
||||||
collectibles: collectibles.filter(c => c.id !== itemId),
|
|
||||||
score: score + item.value
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
takeDamage: (amount: number) => {
|
|
||||||
const { player, gameStatus } = get();
|
|
||||||
if (player.isInvincible) return;
|
|
||||||
|
|
||||||
const newHealth = player.health - amount;
|
|
||||||
set({
|
|
||||||
player: {
|
|
||||||
...player,
|
|
||||||
health: newHealth
|
|
||||||
},
|
|
||||||
gameStatus: newHealth <= 0 ? 'GAME_OVER' : gameStatus
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
activatePowerUp: (powerUpId: string) => {
|
|
||||||
const { player, powerUps } = get();
|
|
||||||
const powerUp = powerUps.find(p => p.id === powerUpId);
|
|
||||||
if (!powerUp) return;
|
|
||||||
|
|
||||||
set({
|
|
||||||
player: {
|
|
||||||
...player,
|
|
||||||
powerUps: [...player.powerUps, powerUp]
|
|
||||||
},
|
|
||||||
powerUps: powerUps.filter(p => p.id !== powerUpId)
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentPlayer = get().player;
|
|
||||||
set({
|
|
||||||
player: {
|
|
||||||
...currentPlayer,
|
|
||||||
powerUps: currentPlayer.powerUps.filter(p => p.id !== powerUp.id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, powerUp.duration);
|
|
||||||
},
|
|
||||||
|
|
||||||
startNewGame: () => set({
|
|
||||||
player: {
|
player: {
|
||||||
position: { x: 50, y: 50 },
|
position: { x: 50, y: 50 },
|
||||||
health: 100,
|
health: 100,
|
||||||
speed: 5,
|
speed: 5,
|
||||||
powerUps: [],
|
powerUps: [],
|
||||||
isInvincible: false,
|
isInvincible: false,
|
||||||
hasKey: false
|
hasKey: false,
|
||||||
},
|
},
|
||||||
enemies: [],
|
enemies: [],
|
||||||
collectibles: [],
|
collectibles: [],
|
||||||
powerUps: [],
|
powerUps: [],
|
||||||
score: 0,
|
score: 0,
|
||||||
level: 1,
|
level: 1,
|
||||||
gameStatus: 'PLAYING',
|
gameStatus: "MENU",
|
||||||
timePlayed: 0
|
highScores: [],
|
||||||
}),
|
timePlayed: 0,
|
||||||
|
|
||||||
pauseGame: () => set({ gameStatus: 'PAUSED' }),
|
movePlayer: (direction: Position) => {
|
||||||
resumeGame: () => set({ gameStatus: 'PLAYING' })
|
const { player } = get();
|
||||||
|
set({
|
||||||
|
player: {
|
||||||
|
...player,
|
||||||
|
position: {
|
||||||
|
x: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
player.position.x + direction.x * player.speed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
y: Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
player.position.y + direction.y * player.speed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateEnemies: () => {
|
||||||
|
const { enemies, player } = get();
|
||||||
|
const updatedEnemies = enemies.map((enemy) => {
|
||||||
|
const dx = player.position.x - enemy.position.x;
|
||||||
|
const dy = player.position.y - enemy.position.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...enemy,
|
||||||
|
direction: {
|
||||||
|
x: dx / distance,
|
||||||
|
y: dy / distance,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: enemy.position.x + enemy.direction.x * enemy.speed,
|
||||||
|
y: enemy.position.y + enemy.direction.y * enemy.speed,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ enemies: updatedEnemies });
|
||||||
|
},
|
||||||
|
|
||||||
|
collectItem: (itemId: string) => {
|
||||||
|
const { collectibles, score } = get();
|
||||||
|
const item = collectibles.find((c) => c.id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
set({
|
||||||
|
collectibles: collectibles.filter((c) => c.id !== itemId),
|
||||||
|
score: score + item.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
takeDamage: (amount: number) => {
|
||||||
|
const { player, gameStatus } = get();
|
||||||
|
if (player.isInvincible) return;
|
||||||
|
|
||||||
|
const newHealth = player.health - amount;
|
||||||
|
set({
|
||||||
|
player: {
|
||||||
|
...player,
|
||||||
|
health: newHealth,
|
||||||
|
},
|
||||||
|
gameStatus: newHealth <= 0 ? "GAME_OVER" : gameStatus,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
activatePowerUp: (powerUpId: string) => {
|
||||||
|
const { player, powerUps } = get();
|
||||||
|
const powerUp = powerUps.find((p) => p.id === powerUpId);
|
||||||
|
if (!powerUp) return;
|
||||||
|
|
||||||
|
set({
|
||||||
|
player: {
|
||||||
|
...player,
|
||||||
|
powerUps: [...player.powerUps, powerUp],
|
||||||
|
},
|
||||||
|
powerUps: powerUps.filter((p) => p.id !== powerUpId),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentPlayer = get().player;
|
||||||
|
set({
|
||||||
|
player: {
|
||||||
|
...currentPlayer,
|
||||||
|
powerUps: currentPlayer.powerUps.filter(
|
||||||
|
(p) => p.id !== powerUp.id,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, powerUp.duration);
|
||||||
|
},
|
||||||
|
|
||||||
|
startNewGame: () =>
|
||||||
|
set({
|
||||||
|
player: {
|
||||||
|
position: { x: 50, y: 50 },
|
||||||
|
health: 100,
|
||||||
|
speed: 5,
|
||||||
|
powerUps: [],
|
||||||
|
isInvincible: false,
|
||||||
|
hasKey: false,
|
||||||
|
},
|
||||||
|
enemies: [],
|
||||||
|
collectibles: [],
|
||||||
|
powerUps: [],
|
||||||
|
score: 0,
|
||||||
|
level: 1,
|
||||||
|
gameStatus: "PLAYING",
|
||||||
|
timePlayed: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
pauseGame: () => set({ gameStatus: "PAUSED" }),
|
||||||
|
resumeGame: () => set({ gameStatus: "PLAYING" }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useGameStore;
|
export default useGameStore;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import type { GithubRepo } from '@/types';
|
import type { GithubRepo } from "@/types";
|
||||||
|
|
||||||
const useGithubRepos = () => {
|
const useGithubRepos = () => {
|
||||||
const [repos, setRepos] = useState<GithubRepo[]>([]);
|
const [repos, setRepos] = useState<GithubRepo[]>([]);
|
||||||
|
|
@ -9,37 +9,48 @@ const useGithubRepos = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchRepos = async () => {
|
const fetchRepos = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.github.com/users/EndofTimee/repos?sort=updated');
|
const response = await fetch(
|
||||||
|
"https://api.github.com/users/EndofTimee/repos?sort=updated",
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to fetch repositories');
|
throw new Error("Failed to fetch repositories");
|
||||||
}
|
}
|
||||||
const reposData = await response.json() as GithubRepo[];
|
const reposData = (await response.json()) as GithubRepo[];
|
||||||
|
|
||||||
const repoDetails = await Promise.all(
|
const repoDetails = await Promise.all(
|
||||||
reposData.map(async (repo: GithubRepo) => {
|
reposData.map(async (repo: GithubRepo) => {
|
||||||
try {
|
try {
|
||||||
const languagesResponse = await fetch(repo.languages_url);
|
const languagesResponse = await fetch(
|
||||||
const languages = await languagesResponse.json() as Record<string, number>;
|
repo.languages_url,
|
||||||
|
);
|
||||||
|
const languages =
|
||||||
|
(await languagesResponse.json()) as Record<
|
||||||
|
string,
|
||||||
|
number
|
||||||
|
>;
|
||||||
return {
|
return {
|
||||||
...repo,
|
...repo,
|
||||||
languages: Object.keys(languages)
|
languages: Object.keys(languages),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching languages for ${repo.name}:`, error);
|
console.error(
|
||||||
|
`Error fetching languages for ${repo.name}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...repo,
|
...repo,
|
||||||
languages: []
|
languages: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
setRepos(repoDetails);
|
setRepos(repoDetails);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
console.error('Error fetching repos:', err);
|
console.error("Error fetching repos:", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react'
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from "react-dom/client";
|
||||||
import App from '@/App'
|
import App from "@/App";
|
||||||
import '@/styles/index.css'
|
import "@/styles/index.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import FoxCard from '@/components/FoxCard';
|
import FoxCard from "@/components/FoxCard";
|
||||||
import { Code, Cpu } from 'lucide-react';
|
import { Code, Cpu } from "lucide-react";
|
||||||
|
|
||||||
const APCSPPage = () => {
|
const APCSPPage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<FoxCard className="header-card">
|
<FoxCard className="header-card">
|
||||||
<h1 className="text-glow">AP Computer Science Principles</h1>
|
<h1 className="text-glow">AP Computer Science Principles</h1>
|
||||||
<p className="text-gradient">Exploring the foundations of modern computing</p>
|
<p className="text-gradient">
|
||||||
|
Exploring the foundations of modern computing
|
||||||
|
</p>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
||||||
<div className="content-grid">
|
<div className="content-grid">
|
||||||
|
|
@ -15,7 +17,10 @@ const APCSPPage = () => {
|
||||||
<Code size={24} className="text-accent-primary" />
|
<Code size={24} className="text-accent-primary" />
|
||||||
<h2>Programming Concepts</h2>
|
<h2>Programming Concepts</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>Learn the creative aspects of programming, abstractions, and algorithms</p>
|
<p>
|
||||||
|
Learn the creative aspects of programming, abstractions,
|
||||||
|
and algorithms
|
||||||
|
</p>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
||||||
<FoxCard>
|
<FoxCard>
|
||||||
|
|
@ -24,8 +29,8 @@ const APCSPPage = () => {
|
||||||
<h2>Project Demo</h2>
|
<h2>Project Demo</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="project-demo">
|
<div className="project-demo">
|
||||||
<iframe
|
<iframe
|
||||||
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
|
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="400"
|
height="400"
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
import { Gamepad2, Code, Music } from 'lucide-react';
|
import { Gamepad2, Code, Music } from "lucide-react";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import FoxCard from '@/components/FoxCard';
|
import FoxCard from "@/components/FoxCard";
|
||||||
import MusicDisplay from '@/components/MusicDisplay';
|
import MusicDisplay from "@/components/MusicDisplay";
|
||||||
import { calculatePreciseAge } from '@/utils/dateUtils';
|
import { calculatePreciseAge } from "@/utils/dateUtils";
|
||||||
|
|
||||||
const AboutPage = () => {
|
const AboutPage = () => {
|
||||||
const [age, setAge] = useState(calculatePreciseAge(new Date("2009-05-15")));
|
// const [age, setAge] = useState(calculatePreciseAge(new Date("date")));
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
setAge(calculatePreciseAge(new Date("2009-05-15")));
|
// setAge(calculatePreciseAge(new Date("date")));
|
||||||
}, 50);
|
// }, 50);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
// return () => clearInterval(interval);
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<FoxCard className="header-card">
|
<FoxCard className="header-card">
|
||||||
<h1 className="text-glow">About Me</h1>
|
<h1 className="text-glow">About Me</h1>
|
||||||
<p className="text-gradient">
|
<p className="text-gradient">
|
||||||
End • ProtoFoxes • They/Them • {age} years old • Programmer & Streamer
|
End • They/It/She • Programmer & Streamer
|
||||||
</p>
|
</p>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
||||||
|
|
@ -43,16 +43,16 @@ const AboutPage = () => {
|
||||||
<h2>Streaming</h2>
|
<h2>Streaming</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Find me on{' '}
|
Find me on{" "}
|
||||||
<a
|
<a
|
||||||
href="https://twitch.tv/EndofTimee"
|
href="https://twitch.tv/EndofTimee"
|
||||||
className="text-accent-neon hover:text-glow"
|
className="text-accent-neon hover:text-glow"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Twitch
|
Twitch
|
||||||
</a>
|
</a>{" "}
|
||||||
{' '}playing FiveM and other games!
|
playing FiveM and other games!
|
||||||
</p>
|
</p>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
||||||
|
|
@ -68,4 +68,4 @@ const AboutPage = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AboutPage;
|
export default AboutPage;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import FoxCard from '@/components/FoxCard';
|
import FoxCard from "@/components/FoxCard";
|
||||||
import GithubRepos from '@/components/GithubRepos';
|
import GithubRepos from "@/components/GithubRepos";
|
||||||
import useGithubRepos from '@/hooks/useGithubRepos';
|
import useGithubRepos from "@/hooks/useGithubRepos";
|
||||||
import LoadingFox from '@/components/LoadingFox';
|
import LoadingFox from "@/components/LoadingFox";
|
||||||
|
|
||||||
const ProjectsPage = () => {
|
const ProjectsPage = () => {
|
||||||
const { repos, loading, error } = useGithubRepos();
|
const { repos, loading, error } = useGithubRepos();
|
||||||
|
|
@ -10,7 +10,9 @@ const ProjectsPage = () => {
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<FoxCard className="header-card">
|
<FoxCard className="header-card">
|
||||||
<h1 className="text-glow">My Projects</h1>
|
<h1 className="text-glow">My Projects</h1>
|
||||||
<p className="text-gradient">Exploring code, one repo at a time</p>
|
<p className="text-gradient">
|
||||||
|
Exploring code, one repo at a time
|
||||||
|
</p>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -18,7 +20,10 @@ const ProjectsPage = () => {
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<FoxCard className="error-card">
|
<FoxCard className="error-card">
|
||||||
<p>Oops! Something went wrong fetching the repositories.</p>
|
<p>Oops! Something went wrong fetching the repositories.</p>
|
||||||
<button onClick={() => window.location.reload()} className="retry-button">
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="retry-button"
|
||||||
|
>
|
||||||
Try Again
|
Try Again
|
||||||
</button>
|
</button>
|
||||||
</FoxCard>
|
</FoxCard>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => {
|
const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => {
|
||||||
if (onPerfEntry && typeof onPerfEntry === 'function') {
|
if (onPerfEntry && typeof onPerfEntry === "function") {
|
||||||
import('web-vitals').then((vitals) => {
|
import("web-vitals")
|
||||||
const { onCLS, onFID, onFCP, onLCP, onTTFB } = vitals;
|
.then((vitals) => {
|
||||||
onCLS(onPerfEntry);
|
const { onCLS, onFID, onFCP, onLCP, onTTFB } = vitals;
|
||||||
onFID(onPerfEntry);
|
onCLS(onPerfEntry);
|
||||||
onFCP(onPerfEntry);
|
onFID(onPerfEntry);
|
||||||
onLCP(onPerfEntry);
|
onFCP(onPerfEntry);
|
||||||
onTTFB(onPerfEntry);
|
onLCP(onPerfEntry);
|
||||||
}).catch((error) => {
|
onTTFB(onPerfEntry);
|
||||||
console.error('Error loading web-vitals:', error);
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
console.error("Error loading web-vitals:", error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,138 +1,142 @@
|
||||||
/* src/App.css */
|
/* src/App.css */
|
||||||
:root {
|
:root {
|
||||||
--background-primary: #1a0b2e;
|
--background-primary: #1a0b2e;
|
||||||
--background-secondary: #2f1c54;
|
--background-secondary: #2f1c54;
|
||||||
--accent-primary: #9d4edd;
|
--accent-primary: #9d4edd;
|
||||||
--accent-neon: #b249f8;
|
--accent-neon: #b249f8;
|
||||||
--text-glow: #e0aaff;
|
--text-glow: #e0aaff;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--dark-accent: #240046;
|
--dark-accent: #240046;
|
||||||
}
|
}
|
||||||
/* Custom Scrollbar */
|
/* Custom Scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--accent-primary);
|
background: var(--accent-primary);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--accent-neon);
|
background: var(--accent-neon);
|
||||||
box-shadow: 0 0 10px var(--text-glow);
|
box-shadow: 0 0 10px var(--text-glow);
|
||||||
}
|
}
|
||||||
/* Particle Effects */
|
/* Particle Effects */
|
||||||
.particle-container {
|
.particle-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.particle {
|
.particle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: var(--text-glow);
|
background: var(--text-glow);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: particleFloat linear infinite;
|
animation: particleFloat linear infinite;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@keyframes particleFloat {
|
@keyframes particleFloat {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(100vh) scale(0);
|
transform: translateY(100vh) scale(0);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(-20vh) scale(1);
|
transform: translateY(-20vh) scale(1);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Main Layout */
|
/* Main Layout */
|
||||||
.app-container {
|
.app-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: linear-gradient(135deg, var(--background-primary), var(--background-secondary));
|
background: linear-gradient(
|
||||||
color: var(--text-primary);
|
135deg,
|
||||||
font-family: 'Inter', sans-serif;
|
var(--background-primary),
|
||||||
position: relative;
|
var(--background-secondary)
|
||||||
overflow-x: hidden;
|
);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
/* Header Styles */
|
/* Header Styles */
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4rem 2rem;
|
padding: 4rem 2rem;
|
||||||
}
|
}
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
/* Content Sections */
|
/* Content Sections */
|
||||||
.content-section {
|
.content-section {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background: rgba(26, 11, 46, 0.5);
|
background: rgba(26, 11, 46, 0.5);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
/* Interests Grid */
|
/* Interests Grid */
|
||||||
.interests-grid {
|
.interests-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
.interest-card {
|
.interest-card {
|
||||||
background: rgba(47, 28, 84, 0.3);
|
background: rgba(47, 28, 84, 0.3);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(157, 78, 221, 0.2);
|
border: 1px solid rgba(157, 78, 221, 0.2);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.interest-card:hover {
|
.interest-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
border-color: var(--accent-neon);
|
border-color: var(--accent-neon);
|
||||||
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
||||||
}
|
}
|
||||||
/* Twitch Button */
|
/* Twitch Button */
|
||||||
.twitch-button {
|
.twitch-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
background: #9146ff;
|
background: #9146ff;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.twitch-button:hover {
|
.twitch-button:hover {
|
||||||
background: #7c2bff;
|
background: #7c2bff;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 0 15px rgba(145, 70, 255, 0.5);
|
box-shadow: 0 0 15px rgba(145, 70, 255, 0.5);
|
||||||
}
|
}
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
.content-section {
|
.content-section {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
.interests-grid {
|
.interests-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,523 +1,626 @@
|
||||||
|
|
||||||
/* Full screen overlay */
|
/* Full screen overlay */
|
||||||
.endos-boot-container {
|
.endos-boot-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-family: 'Orbitron', monospace;
|
font-family: "Orbitron", monospace;
|
||||||
color: #00E5FF;
|
color: #00e5ff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scanning animation effect */
|
/* Scanning animation effect */
|
||||||
.boot-scan-line {
|
.boot-scan-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(
|
||||||
rgba(0, 229, 255, 0) 0%,
|
90deg,
|
||||||
rgba(0, 229, 255, 0.8) 50%,
|
rgba(0, 229, 255, 0) 0%,
|
||||||
rgba(0, 229, 255, 0) 100%);
|
rgba(0, 229, 255, 0.8) 50%,
|
||||||
box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
|
rgba(0, 229, 255, 0) 100%
|
||||||
animation: scanAnimation 3s linear infinite;
|
);
|
||||||
pointer-events: none;
|
box-shadow: 0 0 10px rgba(0, 229, 255, 0.8);
|
||||||
|
animation: scanAnimation 3s linear infinite;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scanAnimation {
|
@keyframes scanAnimation {
|
||||||
0% { top: -10px; }
|
0% {
|
||||||
100% { top: 100vh; }
|
top: -10px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 100vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fox ear decorations */
|
/* Fox ear decorations */
|
||||||
.visor-left-ear,
|
.visor-left-ear,
|
||||||
.visor-right-ear {
|
.visor-right-ear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: #ff9466;
|
background-color: #ff9466;
|
||||||
top: -20px;
|
top: -20px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visor-left-ear {
|
.visor-left-ear {
|
||||||
left: calc(50% - 100px);
|
left: calc(50% - 100px);
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
border-radius: 0 0 0 20px;
|
border-radius: 0 0 0 20px;
|
||||||
transform-origin: bottom right;
|
transform-origin: bottom right;
|
||||||
animation: earTwitch 4s ease-in-out infinite;
|
animation: earTwitch 4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visor-right-ear {
|
.visor-right-ear {
|
||||||
right: calc(50% - 100px);
|
right: calc(50% - 100px);
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
border-radius: 0 0 20px 0;
|
border-radius: 0 0 20px 0;
|
||||||
transform-origin: bottom left;
|
transform-origin: bottom left;
|
||||||
animation: earTwitchRight 4s ease-in-out infinite;
|
animation: earTwitchRight 4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes earTwitch {
|
@keyframes earTwitch {
|
||||||
0%, 100% { transform: rotate(45deg); }
|
0%,
|
||||||
50% { transform: rotate(30deg); }
|
100% {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(30deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes earTwitchRight {
|
@keyframes earTwitchRight {
|
||||||
0%, 100% { transform: rotate(-45deg); }
|
0%,
|
||||||
50% { transform: rotate(-30deg); }
|
100% {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(-30deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main visor frame */
|
/* Main visor frame */
|
||||||
.boot-visor-frame {
|
.boot-visor-frame {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
border: 2px solid #00E5FF;
|
border: 2px solid #00e5ff;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
|
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: rgba(0, 10, 15, 0.95);
|
background-color: rgba(0, 10, 15, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-visor {
|
.boot-visor {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Visor top and bottom glowing lines */
|
/* Visor top and bottom glowing lines */
|
||||||
.visor-line {
|
.visor-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background-color: #00E5FF;
|
background-color: #00e5ff;
|
||||||
box-shadow: 0 0 10px #00E5FF;
|
box-shadow: 0 0 10px #00e5ff;
|
||||||
animation: glowPulse 2s ease-in-out infinite;
|
animation: glowPulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visor-line.top {
|
.visor-line.top {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visor-line.bottom {
|
.visor-line.bottom {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glowPulse {
|
@keyframes glowPulse {
|
||||||
0%, 100% { opacity: 1; box-shadow: 0 0 10px #00E5FF, 0 0 20px rgba(0, 229, 255, 0.5); }
|
0%,
|
||||||
50% { opacity: 0.7; box-shadow: 0 0 15px #00E5FF, 0 0 30px rgba(0, 229, 255, 0.7); }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px #00e5ff,
|
||||||
|
0 0 20px rgba(0, 229, 255, 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
box-shadow:
|
||||||
|
0 0 15px #00e5ff,
|
||||||
|
0 0 30px rgba(0, 229, 255, 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Boot content area */
|
/* Boot content area */
|
||||||
.boot-content {
|
.boot-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
transform: translateY(0); /* Reset to prevent z-index conflicts */
|
transform: translateY(0); /* Reset to prevent z-index conflicts */
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-content.active {
|
.boot-content.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Common styles for all boot stages */
|
/* Common styles for all boot stages */
|
||||||
.boot-stage {
|
.boot-stage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: rgba(0, 0, 0, 0.6); /* Darker background to prevent seeing through */
|
background-color: rgba(
|
||||||
border: 1px solid rgba(0, 229, 255, 0.3);
|
0,
|
||||||
border-radius: 5px;
|
0,
|
||||||
opacity: 0;
|
0,
|
||||||
visibility: hidden;
|
0.6
|
||||||
display: none; /* Added display:none to completely remove from flow */
|
); /* Darker background to prevent seeing through */
|
||||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
border: 1px solid rgba(0, 229, 255, 0.3);
|
||||||
max-height: 80%;
|
border-radius: 5px;
|
||||||
overflow-y: auto;
|
opacity: 0;
|
||||||
z-index: 1; /* Ensure z-index is consistent */
|
visibility: hidden;
|
||||||
|
display: none; /* Added display:none to completely remove from flow */
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
visibility 0.3s ease;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1; /* Ensure z-index is consistent */
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-stage.active {
|
.boot-stage.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
display: block; /* Make it visible in the flow */
|
display: block; /* Make it visible in the flow */
|
||||||
z-index: 10; /* Higher z-index when active */
|
z-index: 10; /* Higher z-index when active */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar but allow scrolling */
|
/* Hide scrollbar but allow scrolling */
|
||||||
.boot-stage::-webkit-scrollbar {
|
.boot-stage::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-stage::-webkit-scrollbar-track {
|
.boot-stage::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-stage::-webkit-scrollbar-thumb {
|
.boot-stage::-webkit-scrollbar-thumb {
|
||||||
background: rgba(0, 229, 255, 0.3);
|
background: rgba(0, 229, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BIOS Stage */
|
/* BIOS Stage */
|
||||||
.bios-header {
|
.bios-header {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: #FF005C;
|
color: #ff005c;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-text-line {
|
.boot-text-line {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: #8be9fd;
|
color: #8be9fd;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: typeWriter 1s steps(50, end);
|
animation: typeWriter 1s steps(50, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes typeWriter {
|
@keyframes typeWriter {
|
||||||
from { width: 0; }
|
from {
|
||||||
to { width: 100%; }
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* System Scan Stage */
|
/* System Scan Stage */
|
||||||
.scan-header {
|
.scan-header {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #FF005C;
|
color: #ff005c;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-progress-container {
|
.scan-progress-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
border: 1px solid rgba(0, 229, 255, 0.5);
|
border: 1px solid rgba(0, 229, 255, 0.5);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-progress-bar {
|
.scan-progress-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #00E5FF, #FF005C);
|
background: linear-gradient(90deg, #00e5ff, #ff005c);
|
||||||
width: 0;
|
width: 0;
|
||||||
animation: progressFill 2s ease-in-out forwards;
|
animation: progressFill 2s ease-in-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressFill {
|
@keyframes progressFill {
|
||||||
0% { width: 0; }
|
0% {
|
||||||
100% { width: 100%; }
|
width: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scan-detail {
|
.scan-detail {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: #8be9fd;
|
color: #8be9fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Module Loading Stage */
|
/* Module Loading Stage */
|
||||||
.module-header {
|
.module-header {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #FF005C;
|
color: #ff005c;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modules-grid {
|
.modules-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-item {
|
.module-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
border: 1px solid rgba(0, 229, 255, 0.2);
|
border: 1px solid rgba(0, 229, 255, 0.2);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
animation: moduleLoad 1s ease forwards;
|
animation: moduleLoad 1s ease forwards;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-item:nth-child(1) { animation-delay: 0.2s; }
|
.module-item:nth-child(1) {
|
||||||
.module-item:nth-child(2) { animation-delay: 0.4s; }
|
animation-delay: 0.2s;
|
||||||
.module-item:nth-child(3) { animation-delay: 0.6s; }
|
}
|
||||||
.module-item:nth-child(4) { animation-delay: 0.8s; }
|
.module-item:nth-child(2) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.module-item:nth-child(3) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
.module-item:nth-child(4) {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes moduleLoad {
|
@keyframes moduleLoad {
|
||||||
0% { opacity: 0; transform: translateX(-20px); }
|
0% {
|
||||||
100% { opacity: 1; transform: translateX(0); }
|
opacity: 0;
|
||||||
|
transform: translateX(-20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-icon {
|
.module-icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #00E5FF;
|
background-color: #00e5ff;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
animation: iconPulse 2s infinite;
|
animation: iconPulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes iconPulse {
|
@keyframes iconPulse {
|
||||||
0%, 100% { transform: scale(1); box-shadow: 0 0 5px #00E5FF; }
|
0%,
|
||||||
50% { transform: scale(1.1); box-shadow: 0 0 10px #00E5FF; }
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 5px #00e5ff;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 10px #00e5ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-name {
|
.module-name {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fox Protocol Stage */
|
/* Fox Protocol Stage */
|
||||||
.fox-header {
|
.fox-header {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #ff9466;
|
color: #ff9466;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-shadow: 0 0 10px rgba(255, 148, 102, 0.7);
|
text-shadow: 0 0 10px rgba(255, 148, 102, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-trait {
|
.fox-trait {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: "Courier New", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
color: #ff9466;
|
color: #ff9466;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: traitAppear 0.5s ease forwards;
|
animation: traitAppear 0.5s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-trait:nth-child(2) { animation-delay: 0.2s; }
|
.fox-trait:nth-child(2) {
|
||||||
.fox-trait:nth-child(3) { animation-delay: 0.4s; }
|
animation-delay: 0.2s;
|
||||||
.fox-trait:nth-child(4) { animation-delay: 0.6s; }
|
}
|
||||||
.fox-trait:nth-child(5) { animation-delay: 0.8s; }
|
.fox-trait:nth-child(3) {
|
||||||
.fox-trait:nth-child(6) { animation-delay: 1s; }
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.fox-trait:nth-child(4) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
.fox-trait:nth-child(5) {
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
.fox-trait:nth-child(6) {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes traitAppear {
|
@keyframes traitAppear {
|
||||||
0% { opacity: 0; transform: translateX(-10px); }
|
0% {
|
||||||
100% { opacity: 1; transform: translateX(0); }
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-trait::before {
|
.fox-trait::before {
|
||||||
content: '>';
|
content: ">";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
color: #ff9466;
|
color: #ff9466;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Logo Display Stage */
|
/* Logo Display Stage */
|
||||||
.logo-display {
|
.logo-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.endos-logo {
|
.endos-logo {
|
||||||
font-size: 48px;
|
font-size: 48px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: linear-gradient(90deg, #ff9466, #00E5FF);
|
background: linear-gradient(90deg, #ff9466, #00e5ff);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
text-fill-color: transparent;
|
text-fill-color: transparent;
|
||||||
text-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
|
text-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: logoAppear 1s ease forwards;
|
animation: logoAppear 1s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-end {
|
.logo-end {
|
||||||
color: #ff9466;
|
color: #ff9466;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-os {
|
.logo-os {
|
||||||
color: #00E5FF;
|
color: #00e5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-subtitle {
|
.logo-subtitle {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation: logoAppear 1s ease forwards 0.5s;
|
animation: logoAppear 1s ease forwards 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logoAppear {
|
@keyframes logoAppear {
|
||||||
0% { opacity: 0; transform: scale(0.9); }
|
0% {
|
||||||
100% { opacity: 1; transform: scale(1); }
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* System Ready Stage */
|
/* System Ready Stage */
|
||||||
.system-ready {
|
.system-ready {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ready-status {
|
.ready-status {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #00E5FF;
|
color: #00e5ff;
|
||||||
animation: textPulse 2s infinite;
|
animation: textPulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes textPulse {
|
@keyframes textPulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.7; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-message {
|
.welcome-message {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.boot-complete-message {
|
.boot-complete-message {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8be9fd;
|
color: #8be9fd;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Skip button - Enhanced with animation and more prominence */
|
/* Skip button - Enhanced with animation and more prominence */
|
||||||
.skip-button {
|
.skip-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 40px; /* More visible position */
|
bottom: 40px; /* More visible position */
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background-color: rgba(255, 0, 92, 0.3); /* More visible color */
|
background-color: rgba(255, 0, 92, 0.3); /* More visible color */
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid rgba(255, 0, 92, 0.5);
|
border: 1px solid rgba(255, 0, 92, 0.5);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px 20px; /* Larger size */
|
padding: 10px 20px; /* Larger size */
|
||||||
font-family: 'Orbitron', sans-serif;
|
font-family: "Orbitron", sans-serif;
|
||||||
font-size: 14px; /* Larger font */
|
font-size: 14px; /* Larger font */
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
animation: skipPulse 2s infinite; /* Pulsing animation to draw attention */
|
animation: skipPulse 2s infinite; /* Pulsing animation to draw attention */
|
||||||
box-shadow: 0 0 10px rgba(255, 0, 92, 0.3); /* Glow effect */
|
box-shadow: 0 0 10px rgba(255, 0, 92, 0.3); /* Glow effect */
|
||||||
}
|
}
|
||||||
|
|
||||||
.skip-button:hover {
|
.skip-button:hover {
|
||||||
background-color: rgba(255, 0, 92, 0.7);
|
background-color: rgba(255, 0, 92, 0.7);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: rgba(255, 0, 92, 0.8);
|
border-color: rgba(255, 0, 92, 0.8);
|
||||||
box-shadow: 0 0 15px rgba(255, 0, 92, 0.5);
|
box-shadow: 0 0 15px rgba(255, 0, 92, 0.5);
|
||||||
transform: translateX(-50%) scale(1.05);
|
transform: translateX(-50%) scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes skipPulse {
|
@keyframes skipPulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.7; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accessibility */
|
/* Accessibility */
|
||||||
.visually-hidden {
|
.visually-hidden {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: -1px;
|
margin: -1px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip: rect(0, 0, 0, 0);
|
clip: rect(0, 0, 0, 0);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.boot-visor-frame {
|
.boot-visor-frame {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visor-left-ear, .visor-right-ear {
|
.visor-left-ear,
|
||||||
width: 30px;
|
.visor-right-ear {
|
||||||
height: 30px;
|
width: 30px;
|
||||||
top: -15px;
|
height: 30px;
|
||||||
}
|
top: -15px;
|
||||||
|
}
|
||||||
.visor-left-ear {
|
|
||||||
left: calc(50% - 70px);
|
.visor-left-ear {
|
||||||
}
|
left: calc(50% - 70px);
|
||||||
|
}
|
||||||
.visor-right-ear {
|
|
||||||
right: calc(50% - 70px);
|
.visor-right-ear {
|
||||||
}
|
right: calc(50% - 70px);
|
||||||
|
}
|
||||||
.boot-visor {
|
|
||||||
padding: 15px;
|
.boot-visor {
|
||||||
}
|
padding: 15px;
|
||||||
|
}
|
||||||
.boot-stage {
|
|
||||||
width: 90%;
|
.boot-stage {
|
||||||
padding: 15px;
|
width: 90%;
|
||||||
}
|
padding: 15px;
|
||||||
|
}
|
||||||
.modules-grid {
|
|
||||||
grid-template-columns: 1fr;
|
.modules-grid {
|
||||||
}
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.bios-header, .scan-header, .module-header, .fox-header {
|
|
||||||
font-size: 18px;
|
.bios-header,
|
||||||
}
|
.scan-header,
|
||||||
|
.module-header,
|
||||||
.endos-logo {
|
.fox-header {
|
||||||
font-size: 36px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-subtitle {
|
.endos-logo {
|
||||||
font-size: 14px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ready-status {
|
.logo-subtitle {
|
||||||
font-size: 24px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ready-status {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Very small screens */
|
/* Very small screens */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.boot-stage {
|
.boot-stage {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bios-header, .scan-header, .module-header, .fox-header {
|
.bios-header,
|
||||||
font-size: 16px;
|
.scan-header,
|
||||||
}
|
.module-header,
|
||||||
|
.fox-header {
|
||||||
.boot-text-line, .scan-detail, .fox-trait {
|
font-size: 16px;
|
||||||
font-size: 12px;
|
}
|
||||||
}
|
|
||||||
|
.boot-text-line,
|
||||||
.skip-button {
|
.scan-detail,
|
||||||
padding: 8px 16px;
|
.fox-trait {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skip-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
.fox-card {
|
.fox-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--gradient-card);
|
background: var(--gradient-card);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border: 1px solid rgba(157, 78, 221, 0.2);
|
border: 1px solid rgba(157, 78, 221, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.fox-ear {
|
.fox-ear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background: var(--fox-pink);
|
background: var(--fox-pink);
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
.fox-ear-left {
|
.fox-ear-left {
|
||||||
top: -15px;
|
top: -15px;
|
||||||
left: -15px;
|
left: -15px;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
border-radius: 0 0 0 15px;
|
border-radius: 0 0 0 15px;
|
||||||
}
|
}
|
||||||
.fox-ear-right {
|
.fox-ear-right {
|
||||||
top: -15px;
|
top: -15px;
|
||||||
right: -15px;
|
right: -15px;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
border-radius: 0 0 15px 0;
|
border-radius: 0 0 15px 0;
|
||||||
}
|
}
|
||||||
.fox-card:hover .fox-ear {
|
.fox-card:hover .fox-ear {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,60 @@
|
||||||
.github-repos-container {
|
.github-repos-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
.repos-grid {
|
.repos-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
}
|
}
|
||||||
.repo-card {
|
.repo-card {
|
||||||
background: rgba(47, 28, 84, 0.3);
|
background: rgba(47, 28, 84, 0.3);
|
||||||
border: 1px solid rgba(157, 78, 221, 0.2);
|
border: 1px solid rgba(157, 78, 221, 0.2);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.repo-card:hover {
|
.repo-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
border-color: var(--accent-neon);
|
border-color: var(--accent-neon);
|
||||||
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
||||||
}
|
}
|
||||||
.repo-name {
|
.repo-name {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.repo-name:hover {
|
.repo-name:hover {
|
||||||
color: var(--accent-neon);
|
color: var(--accent-neon);
|
||||||
}
|
}
|
||||||
.repo-description {
|
.repo-description {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.repo-language {
|
.repo-language {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
background: rgba(157, 78, 221, 0.2);
|
background: rgba(157, 78, 221, 0.2);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
.repo-languages {
|
.repo-languages {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.language-tag {
|
.language-tag {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
background: rgba(157, 78, 221, 0.1);
|
background: rgba(157, 78, 221, 0.1);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,47 @@
|
||||||
.loading-fox-container {
|
.loading-fox-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
}
|
}
|
||||||
.fox-loader {
|
.fox-loader {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
.fox-face {
|
.fox-face {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
background: var(--fox-orange);
|
background: var(--fox-orange);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: bounce 1s ease-in-out infinite;
|
animation: bounce 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.fox-ears {
|
.fox-ears {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -15px;
|
top: -15px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.ear {
|
.ear {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: var(--fox-orange);
|
background: var(--fox-orange);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
.ear.left {
|
.ear.left {
|
||||||
transform: rotate(-30deg);
|
transform: rotate(-30deg);
|
||||||
}
|
}
|
||||||
.ear.right {
|
.ear.right {
|
||||||
transform: rotate(30deg);
|
transform: rotate(30deg);
|
||||||
}
|
}
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%,
|
||||||
50% { transform: translateY(-10px); }
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,22 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
background-color: rgba(26,11,46,0.5);
|
background-color: rgba(26, 11, 46, 0.5);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
border: 1px solid rgb(157,78,221,0.2);
|
border: 1px solid rgb(157, 78, 221, 0.2);
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle-container:hover {
|
.theme-toggle-container:hover {
|
||||||
border-color: rgba(157,78,221,0.4);
|
border-color: rgba(157, 78, 221, 0.4);
|
||||||
box-shadow: 0 4px 12px rgba(157,78,221,0.2);
|
box-shadow: 0 4px 12px rgba(157, 78, 221, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
position:relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -100,14 +100,14 @@ input:checked + .toggle-track .toggle-indicator {
|
||||||
}
|
}
|
||||||
|
|
||||||
input:checked + .toggle-track .toggle.visor {
|
input:checked + .toggle-track .toggle.visor {
|
||||||
background-color: #00E5FF;
|
background-color: #00e5ff;
|
||||||
box-shadow: 0 0 5px #00e5FF;
|
box-shadow: 0 0 5px #00e5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-label {
|
.toggle-label {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,8 +128,7 @@ input:checked + .toggle-track .toggle-label {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
input:checked + .toggle-track .toggle-indicator {
|
input:checked + .toggle-track .toggle-indicator {
|
||||||
left: 22px;
|
left: 22px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,188 +1,267 @@
|
||||||
/* Basic Animations */
|
/* Basic Animations */
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(0px); }
|
0%,
|
||||||
50% { transform: translateY(-10px); }
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.6; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glow {
|
@keyframes glow {
|
||||||
0%, 100% { filter: drop-shadow(0 0 2px var(--accent-neon)); }
|
0%,
|
||||||
50% { filter: drop-shadow(0 0 8px var(--accent-neon)); }
|
100% {
|
||||||
|
filter: drop-shadow(0 0 2px var(--accent-neon));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: drop-shadow(0 0 8px var(--accent-neon));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%,
|
||||||
50% { transform: translateY(-10px); }
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeOut {
|
@keyframes fadeOut {
|
||||||
from { opacity: 1; transform: translateY(0); }
|
from {
|
||||||
to { opacity: 0; transform: translateY(10px); }
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
from { transform: translateX(30px); opacity: 0; }
|
from {
|
||||||
to { transform: translateX(0); opacity: 1; }
|
transform: translateX(30px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInLeft {
|
@keyframes slideInLeft {
|
||||||
from { transform: translateX(-30px); opacity: 0; }
|
from {
|
||||||
to { transform: translateX(0); opacity: 1; }
|
transform: translateX(-30px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes wiggle {
|
@keyframes wiggle {
|
||||||
0%, 100% { transform: rotate(0deg); }
|
0%,
|
||||||
25% { transform: rotate(5deg); }
|
100% {
|
||||||
75% { transform: rotate(-5deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* System-specific animations */
|
/* System-specific animations */
|
||||||
@keyframes switchGlitch {
|
@keyframes switchGlitch {
|
||||||
0% { transform: translate(0) skew(0); filter: hue-rotate(0deg); }
|
0% {
|
||||||
20% { transform: translate(-2px, 2px) skew(2deg, -2deg); filter: hue-rotate(90deg); }
|
transform: translate(0) skew(0);
|
||||||
40% { transform: translate(2px, 0) skew(-2deg, 0); filter: hue-rotate(180deg); }
|
filter: hue-rotate(0deg);
|
||||||
60% { transform: translate(0, -2px) skew(0, 2deg); filter: hue-rotate(270deg); }
|
}
|
||||||
80% { transform: translate(-2px, 0) skew(2deg, 0); filter: hue-rotate(360deg); }
|
20% {
|
||||||
100% { transform: translate(0) skew(0); filter: hue-rotate(0deg); }
|
transform: translate(-2px, 2px) skew(2deg, -2deg);
|
||||||
|
filter: hue-rotate(90deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translate(2px, 0) skew(-2deg, 0);
|
||||||
|
filter: hue-rotate(180deg);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translate(0, -2px) skew(0, 2deg);
|
||||||
|
filter: hue-rotate(270deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translate(-2px, 0) skew(2deg, 0);
|
||||||
|
filter: hue-rotate(360deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0) skew(0);
|
||||||
|
filter: hue-rotate(0deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes statusBlink {
|
@keyframes statusBlink {
|
||||||
0%, 49% { opacity: 1; }
|
0%,
|
||||||
50%, 100% { opacity: 0.7; }
|
49% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add animation classes */
|
/* Add animation classes */
|
||||||
.animate-float {
|
.animate-float {
|
||||||
animation: float 3s ease-in-out infinite;
|
animation: float 3s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-pulse {
|
.animate-pulse {
|
||||||
animation: pulse 2s ease-in-out infinite;
|
animation: pulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-glow {
|
.animate-glow {
|
||||||
animation: glow 2s ease-in-out infinite;
|
animation: glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-spin {
|
.animate-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-bounce {
|
.animate-bounce {
|
||||||
animation: bounce 1s ease-in-out infinite;
|
animation: bounce 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
.animate-fade-in {
|
||||||
animation: fadeIn 0.5s ease-in-out forwards;
|
animation: fadeIn 0.5s ease-in-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-out {
|
.animate-fade-out {
|
||||||
animation: fadeOut 0.5s ease-in-out forwards;
|
animation: fadeOut 0.5s ease-in-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-right {
|
.animate-slide-in-right {
|
||||||
animation: slideInRight 0.3s ease-out forwards;
|
animation: slideInRight 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-slide-in-left {
|
.animate-slide-in-left {
|
||||||
animation: slideInLeft 0.3s ease-out forwards;
|
animation: slideInLeft 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-wiggle {
|
.animate-wiggle {
|
||||||
animation: wiggle 1s ease-in-out;
|
animation: wiggle 1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-switch-glitch {
|
.animate-switch-glitch {
|
||||||
animation: switchGlitch 0.5s ease-in-out;
|
animation: switchGlitch 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-status-blink {
|
.animate-status-blink {
|
||||||
animation: statusBlink 1s infinite;
|
animation: statusBlink 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Delay Utilities */
|
/* Delay Utilities */
|
||||||
.delay-100 {
|
.delay-100 {
|
||||||
animation-delay: 100ms;
|
animation-delay: 100ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-200 {
|
.delay-200 {
|
||||||
animation-delay: 200ms;
|
animation-delay: 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-300 {
|
.delay-300 {
|
||||||
animation-delay: 300ms;
|
animation-delay: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-500 {
|
.delay-500 {
|
||||||
animation-delay: 500ms;
|
animation-delay: 500ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-700 {
|
.delay-700 {
|
||||||
animation-delay: 700ms;
|
animation-delay: 700ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-1000 {
|
.delay-1000 {
|
||||||
animation-delay: 1000ms;
|
animation-delay: 1000ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Duration Utilities */
|
/* Duration Utilities */
|
||||||
.duration-300 {
|
.duration-300 {
|
||||||
animation-duration: 300ms;
|
animation-duration: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-500 {
|
.duration-500 {
|
||||||
animation-duration: 500ms;
|
animation-duration: 500ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-700 {
|
.duration-700 {
|
||||||
animation-duration: 700ms;
|
animation-duration: 700ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-1000 {
|
.duration-1000 {
|
||||||
animation-duration: 1000ms;
|
animation-duration: 1000ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-2000 {
|
.duration-2000 {
|
||||||
animation-duration: 2000ms;
|
animation-duration: 2000ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-3000 {
|
.duration-3000 {
|
||||||
animation-duration: 3000ms;
|
animation-duration: 3000ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation behavior */
|
/* Animation behavior */
|
||||||
.animation-once {
|
.animation-once {
|
||||||
animation-iteration-count: 1;
|
animation-iteration-count: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-twice {
|
.animation-twice {
|
||||||
animation-iteration-count: 2;
|
animation-iteration-count: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-infinite {
|
.animation-infinite {
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pause animations when prefers-reduced-motion */
|
/* Pause animations when prefers-reduced-motion */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
* {
|
* {
|
||||||
animation-duration: 0.01ms !important;
|
animation-duration: 0.01ms !important;
|
||||||
animation-iteration-count: 1 !important;
|
animation-iteration-count: 1 !important;
|
||||||
transition-duration: 0.01ms !important;
|
transition-duration: 0.01ms !important;
|
||||||
scroll-behavior: auto !important;
|
scroll-behavior: auto !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,100 +3,104 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background-primary: #1a0b2e;
|
--background-primary: #1a0b2e;
|
||||||
--background-secondary: #2f1c54;
|
--background-secondary: #2f1c54;
|
||||||
--accent-primary: #9d4edd;
|
--accent-primary: #9d4edd;
|
||||||
--accent-neon: #b249f8;
|
--accent-neon: #b249f8;
|
||||||
--text-glow: #e0aaff;
|
--text-glow: #e0aaff;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--dark-accent: #240046;
|
--dark-accent: #240046;
|
||||||
--fox-pink: #ffc6e5;
|
--fox-pink: #ffc6e5;
|
||||||
--fox-pink-glow: #ffadd6;
|
--fox-pink-glow: #ffadd6;
|
||||||
--fox-orange: #ff9466;
|
--fox-orange: #ff9466;
|
||||||
--fox-white: #fff5f9;
|
--fox-white: #fff5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--background-primary);
|
background-color: var(--background-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.text-glow {
|
.text-glow {
|
||||||
text-shadow: 0 0 10px var(--text-glow);
|
text-shadow: 0 0 10px var(--text-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-spacing > * + * {
|
.section-spacing > * + * {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-spacing > * + * {
|
.card-spacing > * + * {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.element-spacing > * + * {
|
.element-spacing > * + * {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.content-wrapper {
|
.content-wrapper {
|
||||||
max-width: 80rem;
|
max-width: 80rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background-color: rgba(157, 78, 221, 0.05);
|
|
||||||
color: var(--accent-neon);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
background-color: rgba(157, 78, 221, 0.1);
|
|
||||||
color: var(--accent-neon);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fox-card {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
border: 1px solid rgba(157, 78, 221, 0.1);
|
|
||||||
background: linear-gradient(135deg, rgba(47, 28, 84, 0.2) 0%, rgba(157, 78, 221, 0.05) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fox-card:hover {
|
|
||||||
border-color: rgba(157, 78, 221, 0.2);
|
|
||||||
box-shadow: 0 2px 8px rgba(157, 78, 221, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 1.5rem;
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.content-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
.nav-link {
|
||||||
.content-grid {
|
display: flex;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
.nav-link:hover {
|
||||||
|
background-color: rgba(157, 78, 221, 0.05);
|
||||||
|
color: var(--accent-neon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: rgba(157, 78, 221, 0.1);
|
||||||
|
color: var(--accent-neon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fox-card {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid rgba(157, 78, 221, 0.1);
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(47, 28, 84, 0.2) 0%,
|
||||||
|
rgba(157, 78, 221, 0.05) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fox-card:hover {
|
||||||
|
border-color: rgba(157, 78, 221, 0.2);
|
||||||
|
box-shadow: 0 2px 8px rgba(157, 78, 221, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/* Default cursor for all elements */
|
/* Default cursor for all elements */
|
||||||
* {
|
* {
|
||||||
cursor: url('@/assets/cursors/default.svg') 16 16, auto;
|
cursor:
|
||||||
|
url("@/assets/cursors/default.svg") 16 16,
|
||||||
|
auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interactive elements cursor */
|
/* Interactive elements cursor */
|
||||||
|
|
@ -12,14 +14,18 @@ input[type="button"],
|
||||||
select,
|
select,
|
||||||
.interactive,
|
.interactive,
|
||||||
.nav-link {
|
.nav-link {
|
||||||
cursor: url('@/assets/cursors/paw.svg') 16 16, pointer;
|
cursor:
|
||||||
|
url("@/assets/cursors/paw.svg") 16 16,
|
||||||
|
pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading state cursor */
|
/* Loading state cursor */
|
||||||
.loading,
|
.loading,
|
||||||
:disabled,
|
:disabled,
|
||||||
[aria-busy="true"] {
|
[aria-busy="true"] {
|
||||||
cursor: url('@/assets/cursors/tail-loading.svg') 16 16, progress;
|
cursor:
|
||||||
|
url("@/assets/cursors/tail-loading.svg") 16 16,
|
||||||
|
progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects for interactive elements */
|
/* Hover effects for interactive elements */
|
||||||
|
|
@ -27,21 +33,21 @@ a:hover,
|
||||||
button:hover,
|
button:hover,
|
||||||
[role="button"]:hover,
|
[role="button"]:hover,
|
||||||
.nav-link:hover {
|
.nav-link:hover {
|
||||||
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
|
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
|
||||||
transition: filter 0.3s ease;
|
transition: filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom cursor regions */
|
/* Custom cursor regions */
|
||||||
.text-select {
|
.text-select {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resize {
|
.resize {
|
||||||
cursor: nw-resize;
|
cursor: nw-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent cursor inheritance in certain cases */
|
/* Prevent cursor inheritance in certain cases */
|
||||||
iframe,
|
iframe,
|
||||||
canvas {
|
canvas {
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,30 @@
|
||||||
/* Game-specific styles */
|
/* Game-specific styles */
|
||||||
.game-active * {
|
.game-active * {
|
||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
.game-viewport {
|
.game-viewport {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
}
|
}
|
||||||
/* Animation utilities */
|
/* Animation utilities */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
/* Game-specific shadows */
|
/* Game-specific shadows */
|
||||||
.fox-shadow {
|
.fox-shadow {
|
||||||
filter: drop-shadow(0 0 8px var(--fox-pink-glow));
|
filter: drop-shadow(0 0 8px var(--fox-pink-glow));
|
||||||
}
|
}
|
||||||
.enemy-shadow {
|
.enemy-shadow {
|
||||||
filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.3));
|
filter: drop-shadow(0 0 8px rgba(255, 0, 0, 0.3));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,173 +1,250 @@
|
||||||
/* src/styles/game.css */
|
/* src/styles/game.css */
|
||||||
@import 'base.css';
|
@import "base.css";
|
||||||
@import 'animations.css';
|
@import "animations.css";
|
||||||
@import 'utilities.css';
|
@import "utilities.css";
|
||||||
@import 'cursor.css';
|
@import "cursor.css";
|
||||||
/* Base game styles */
|
/* Base game styles */
|
||||||
.game-viewport {
|
.game-viewport {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
var(--background-primary) 0%,
|
var(--background-primary) 0%,
|
||||||
var(--background-secondary) 100%
|
var(--background-secondary) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/* Game UI elements */
|
/* Game UI elements */
|
||||||
.game-hud {
|
.game-hud {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
.game-card {
|
.game-card {
|
||||||
background: rgba(47, 28, 84, 0.3);
|
background: rgba(47, 28, 84, 0.3);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
border: 1px solid rgba(157, 78, 221, 0.2);
|
border: 1px solid rgba(157, 78, 221, 0.2);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.game-card:hover {
|
.game-card:hover {
|
||||||
border-color: rgba(178, 73, 248, 0.4);
|
border-color: rgba(178, 73, 248, 0.4);
|
||||||
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
|
||||||
}
|
}
|
||||||
/* Player animations */
|
/* Player animations */
|
||||||
.player-idle {
|
.player-idle {
|
||||||
animation: playerIdle 2s ease-in-out infinite;
|
animation: playerIdle 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.player-move {
|
.player-move {
|
||||||
animation: playerMove 0.3s linear infinite;
|
animation: playerMove 0.3s linear infinite;
|
||||||
}
|
}
|
||||||
.player-hit {
|
.player-hit {
|
||||||
animation: playerHit 0.5s ease-in-out;
|
animation: playerHit 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
@keyframes playerIdle {
|
@keyframes playerIdle {
|
||||||
0%, 100% { transform: translate(-50%, -50%); }
|
0%,
|
||||||
50% { transform: translate(-50%, calc(-50% - 4px)); }
|
100% {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, calc(-50% - 4px));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes playerMove {
|
@keyframes playerMove {
|
||||||
0% { transform: rotate(-2deg); }
|
0% {
|
||||||
50% { transform: rotate(2deg); }
|
transform: rotate(-2deg);
|
||||||
100% { transform: rotate(-2deg); }
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(2deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes playerHit {
|
@keyframes playerHit {
|
||||||
0%, 100% { opacity: 1; }
|
0%,
|
||||||
50% { opacity: 0.5; }
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Enemy animations */
|
/* Enemy animations */
|
||||||
.enemy-patrol {
|
.enemy-patrol {
|
||||||
animation: enemyPatrol 3s linear infinite;
|
animation: enemyPatrol 3s linear infinite;
|
||||||
}
|
}
|
||||||
.enemy-chase {
|
.enemy-chase {
|
||||||
animation: enemyChase 0.5s ease-in-out infinite;
|
animation: enemyChase 0.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes enemyPatrol {
|
@keyframes enemyPatrol {
|
||||||
0% { transform: translateX(0); }
|
0% {
|
||||||
50% { transform: translateX(50px); }
|
transform: translateX(0);
|
||||||
100% { transform: translateX(0); }
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(50px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes enemyChase {
|
@keyframes enemyChase {
|
||||||
0%, 100% { transform: scale(1); }
|
0%,
|
||||||
50% { transform: scale(1.1); }
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Collectible animations */
|
/* Collectible animations */
|
||||||
.collectible {
|
.collectible {
|
||||||
animation: collectibleFloat 2s ease-in-out infinite;
|
animation: collectibleFloat 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.collectible-gem {
|
.collectible-gem {
|
||||||
animation: collectibleGem 3s linear infinite;
|
animation: collectibleGem 3s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes collectibleFloat {
|
@keyframes collectibleFloat {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%,
|
||||||
50% { transform: translateY(-10px); }
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes collectibleGem {
|
@keyframes collectibleGem {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Power-up effects */
|
/* Power-up effects */
|
||||||
.powerup-active {
|
.powerup-active {
|
||||||
animation: powerupPulse 1s ease-in-out infinite;
|
animation: powerupPulse 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
.powerup-shield {
|
.powerup-shield {
|
||||||
animation: shieldRotate 3s linear infinite;
|
animation: shieldRotate 3s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes powerupPulse {
|
@keyframes powerupPulse {
|
||||||
0%, 100% { filter: brightness(1); }
|
0%,
|
||||||
50% { filter: brightness(1.5); }
|
100% {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
filter: brightness(1.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes shieldRotate {
|
@keyframes shieldRotate {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Game effects */
|
/* Game effects */
|
||||||
.particle {
|
.particle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: particleFade 1s ease-out forwards;
|
animation: particleFade 1s ease-out forwards;
|
||||||
}
|
}
|
||||||
@keyframes particleFade {
|
@keyframes particleFade {
|
||||||
0% { transform: scale(1); opacity: 1; }
|
0% {
|
||||||
100% { transform: scale(0); opacity: 0; }
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Game UI animations */
|
/* Game UI animations */
|
||||||
.score-popup {
|
.score-popup {
|
||||||
animation: scorePopup 0.5s ease-out forwards;
|
animation: scorePopup 0.5s ease-out forwards;
|
||||||
}
|
}
|
||||||
.health-change {
|
.health-change {
|
||||||
animation: healthChange 0.5s ease-in-out;
|
animation: healthChange 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
@keyframes scorePopup {
|
@keyframes scorePopup {
|
||||||
0% { transform: scale(0) translateY(0); opacity: 1; }
|
0% {
|
||||||
50% { transform: scale(1.2) translateY(-20px); opacity: 1; }
|
transform: scale(0) translateY(0);
|
||||||
100% { transform: scale(1) translateY(-40px); opacity: 0; }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2) translateY(-20px);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1) translateY(-40px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes healthChange {
|
@keyframes healthChange {
|
||||||
0%, 100% { transform: scale(1); }
|
0%,
|
||||||
50% { transform: scale(1.2); }
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Menu transitions */
|
/* Menu transitions */
|
||||||
.menu-enter {
|
.menu-enter {
|
||||||
animation: menuEnter 0.3s ease-out forwards;
|
animation: menuEnter 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
.menu-exit {
|
.menu-exit {
|
||||||
animation: menuExit 0.3s ease-in forwards;
|
animation: menuExit 0.3s ease-in forwards;
|
||||||
}
|
}
|
||||||
@keyframes menuEnter {
|
@keyframes menuEnter {
|
||||||
from { opacity: 0; transform: scale(0.9); }
|
from {
|
||||||
to { opacity: 1; transform: scale(1); }
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@keyframes menuExit {
|
@keyframes menuExit {
|
||||||
from { opacity: 1; transform: scale(1); }
|
from {
|
||||||
to { opacity: 0; transform: scale(1.1); }
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Custom cursor */
|
/* Custom cursor */
|
||||||
.game-cursor {
|
.game-cursor {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
mix-blend-mode: difference;
|
mix-blend-mode: difference;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
}
|
}
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.game-hud {
|
.game-hud {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
.game-card {
|
.game-card {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Accessibility */
|
/* Accessibility */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.player-idle,
|
.player-idle,
|
||||||
.player-move,
|
.player-move,
|
||||||
.enemy-patrol,
|
.enemy-patrol,
|
||||||
.collectible,
|
.collectible,
|
||||||
.powerup-active {
|
.powerup-active {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,71 @@
|
||||||
/* Page Container */
|
/* Page Container */
|
||||||
.page-container {
|
.page-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
min-height: calc(100vh - 4rem);
|
min-height: calc(100vh - 4rem);
|
||||||
}
|
}
|
||||||
/* Header Card */
|
/* Header Card */
|
||||||
.header-card {
|
.header-card {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--gradient-primary);
|
background: var(--gradient-primary);
|
||||||
}
|
}
|
||||||
.header-card h1 {
|
.header-card h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
/* Content Grid */
|
/* Content Grid */
|
||||||
.content-grid {
|
.content-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
/* Interest List */
|
/* Interest List */
|
||||||
.interest-list {
|
.interest-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
.interest-list li {
|
.interest-list li {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
.interest-list li::before {
|
.interest-list li::before {
|
||||||
content: "🦊";
|
content: "🦊";
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
/* Project Demo */
|
/* Project Demo */
|
||||||
.project-demo {
|
.project-demo {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: var(--border-radius-lg);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page-container {
|
.page-container {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
.header-card h1 {
|
.header-card h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
.content-grid {
|
.content-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Animation Classes */
|
/* Animation Classes */
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.5s ease-in;
|
animation: fadeIn 0.5s ease-in;
|
||||||
}
|
}
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
body.protofox-theme {
|
|
||||||
/* Override core variables with protofox colors */
|
|
||||||
--background-primary: #1a0b2e;
|
|
||||||
--background-secondary: #2f1c54;
|
|
||||||
--accent-primary: #FF7E5F;
|
|
||||||
--accent-secondary: #00E5FF;
|
|
||||||
--text-glow: #4dffc7;
|
|
||||||
--visor-glow: #FF005C;
|
|
||||||
--fox-orange: #ff9466;
|
|
||||||
--fox-orange-dark: #e07242;
|
|
||||||
--proto-blue: #4dc3ff;
|
|
||||||
--proto-cyan: #00e5ff;
|
|
||||||
--proto-neon: #39FFBF;
|
|
||||||
--circuit-line: rgba(0, 229, 255, 0.2);
|
|
||||||
--circuit-node: rgba(0, 229, 255, 0.4);
|
|
||||||
|
|
||||||
/* Apply protofox font overrides */
|
|
||||||
--heading-font: 'Orbitron', sans-serif;
|
|
||||||
--body-font: 'Exo 2', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Font imports and assignments */
|
/* Font imports and assignments */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap');
|
@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700&family=Exo+2:wght@300;400;600&display=swap");
|
||||||
|
|
||||||
|
body.protofox-theme {
|
||||||
|
/* Override core variables with protofox colors */
|
||||||
|
--background-primary: #1a0b2e;
|
||||||
|
--background-secondary: #2f1c54;
|
||||||
|
--accent-primary: #ff7e5f;
|
||||||
|
--accent-secondary: #00e5ff;
|
||||||
|
--text-glow: #4dffc7;
|
||||||
|
--visor-glow: #ff005c;
|
||||||
|
--fox-orange: #ff9466;
|
||||||
|
--fox-orange-dark: #e07242;
|
||||||
|
--proto-blue: #4dc3ff;
|
||||||
|
--proto-cyan: #00e5ff;
|
||||||
|
--proto-neon: #39ffbf;
|
||||||
|
--circuit-line: rgba(0, 229, 255, 0.2);
|
||||||
|
--circuit-node: rgba(0, 229, 255, 0.4);
|
||||||
|
|
||||||
|
/* Apply protofox font overrides */
|
||||||
|
--heading-font: "Orbitron", sans-serif;
|
||||||
|
--body-font: "Exo 2", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
body.protofox-theme h1,
|
body.protofox-theme h1,
|
||||||
body.protofox-theme h2,
|
body.protofox-theme h2,
|
||||||
|
|
@ -28,198 +28,209 @@ body.protofox-theme h3,
|
||||||
body.protofox-theme h4,
|
body.protofox-theme h4,
|
||||||
body.protofox-theme h5,
|
body.protofox-theme h5,
|
||||||
body.protofox-theme h6 {
|
body.protofox-theme h6 {
|
||||||
font-family: var(--heading-font);
|
font-family: var(--heading-font);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme {
|
body.protofox-theme {
|
||||||
font-family: var(--body-font);
|
font-family: var(--body-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add circuit background only in protofox mode */
|
/* Add circuit background only in protofox mode */
|
||||||
body.protofox-theme::before {
|
body.protofox-theme::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background-color: var(--background-primary);
|
background-color: var(--background-primary);
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(var(--circuit-node) 2px, transparent 2px),
|
radial-gradient(var(--circuit-node) 2px, transparent 2px),
|
||||||
linear-gradient(to right, var(--circuit-line) 1px, transparent 1px),
|
linear-gradient(to right, var(--circuit-line) 1px, transparent 1px),
|
||||||
linear-gradient(to bottom, var(--circuit-line) 1px, transparent 1px);
|
linear-gradient(to bottom, var(--circuit-line) 1px, transparent 1px);
|
||||||
background-size: 30px 30px, 30px 30px, 30px 30px;
|
background-size:
|
||||||
background-position: 0 0;
|
30px 30px,
|
||||||
opacity: 0.15;
|
30px 30px,
|
||||||
z-index: -1;
|
30px 30px;
|
||||||
pointer-events: none;
|
background-position: 0 0;
|
||||||
|
opacity: 0.15;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ProtoFox mode cursor */
|
/* ProtoFox mode cursor */
|
||||||
body.protofox-theme {
|
body.protofox-theme {
|
||||||
cursor: url('/cursors/protofox-default.svg') 16 16, auto;
|
cursor:
|
||||||
|
url("/cursors/protofox-default.svg") 16 16,
|
||||||
|
auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme a,
|
body.protofox-theme a,
|
||||||
body.protofox-theme button,
|
body.protofox-theme button,
|
||||||
body.protofox-theme [role="button"],
|
body.protofox-theme [role="button"],
|
||||||
body.protofox-theme .interactive {
|
body.protofox-theme .interactive {
|
||||||
cursor: url('/cursors/protofox-pointer.svg') 16 16, pointer;
|
cursor:
|
||||||
|
url("/cursors/protofox-pointer.svg") 16 16,
|
||||||
|
pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add scan line animation */
|
/* Add scan line animation */
|
||||||
body.protofox-theme::after {
|
body.protofox-theme::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: -10px;
|
top: -10px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--accent-secondary);
|
background: var(--accent-secondary);
|
||||||
opacity: 0.03;
|
opacity: 0.03;
|
||||||
box-shadow: 0 0 8px var(--accent-secondary);
|
box-shadow: 0 0 8px var(--accent-secondary);
|
||||||
animation: protofox-scan 8s linear infinite;
|
animation: protofox-scan 8s linear infinite;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes protofox-scan {
|
@keyframes protofox-scan {
|
||||||
0% { top: -10px; }
|
0% {
|
||||||
100% { top: 100vh; }
|
top: -10px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 100vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhance existing components in protofox mode */
|
/* Enhance existing components in protofox mode */
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
body.protofox-theme .fox-card {
|
body.protofox-theme .fox-card {
|
||||||
border-color: rgba(0, 229, 255, 0.2);
|
border-color: rgba(0, 229, 255, 0.2);
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .fox-card::before {
|
body.protofox-theme .fox-card::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: url('/images/protofox/fur-texture.png');
|
background: url("/images/protofox/fur-texture.png");
|
||||||
opacity: 0.04;
|
opacity: 0.04;
|
||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .fox-card::after {
|
body.protofox-theme .fox-card::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--proto-cyan);
|
background: var(--proto-cyan);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
box-shadow: 0 0 5px var(--proto-cyan);
|
box-shadow: 0 0 5px var(--proto-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
body.protofox-theme button:not(.theme-toggle *),
|
body.protofox-theme button:not(.theme-toggle *),
|
||||||
body.protofox-theme .button {
|
body.protofox-theme .button {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme button:not(.theme-toggle *):before,
|
body.protofox-theme button:not(.theme-toggle *):before,
|
||||||
body.protofox-theme .button:before {
|
body.protofox-theme .button:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--proto-cyan);
|
background: var(--proto-cyan);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme button:hover:before,
|
body.protofox-theme button:hover:before,
|
||||||
body.protofox-theme .button:hover:before {
|
body.protofox-theme .button:hover:before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
box-shadow: 0 0 5px var(--proto-cyan);
|
box-shadow: 0 0 5px var(--proto-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
body.protofox-theme .navbar {
|
body.protofox-theme .navbar {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .navbar::before {
|
body.protofox-theme .navbar::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--proto-cyan);
|
background: var(--proto-cyan);
|
||||||
box-shadow: 0 0 10px var(--proto-cyan);
|
box-shadow: 0 0 10px var(--proto-cyan);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .nav-link {
|
body.protofox-theme .nav-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .nav-link::after {
|
body.protofox-theme .nav-link::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--accent-primary);
|
background: var(--accent-primary);
|
||||||
box-shadow: 0 0 8px var(--accent-primary);
|
box-shadow: 0 0 8px var(--accent-primary);
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .nav-link:hover::after,
|
body.protofox-theme .nav-link:hover::after,
|
||||||
body.protofox-theme .nav-link.active::after {
|
body.protofox-theme .nav-link.active::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* WebRing Enhancements */
|
/* WebRing Enhancements */
|
||||||
body.protofox-theme .webring-button {
|
body.protofox-theme .webring-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .webring-button::before {
|
body.protofox-theme .webring-button::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--proto-cyan);
|
background: var(--proto-cyan);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .webring-button:hover::before {
|
body.protofox-theme .webring-button:hover::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
box-shadow: 0 0 5px var(--proto-cyan);
|
box-shadow: 0 0 5px var(--proto-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* More Foxxos Button Enhancements */
|
/* More Foxxos Button Enhancements */
|
||||||
body.protofox-theme .foxxos-container {
|
body.protofox-theme .foxxos-container {
|
||||||
border-color: rgba(0, 229, 255, 0.2);
|
border-color: rgba(0, 229, 255, 0.2);
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.protofox-theme .foxxos-container::before {
|
body.protofox-theme .foxxos-container::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--proto-cyan);
|
background: var(--proto-cyan);
|
||||||
box-shadow: 0 0 10px var(--proto-cyan);
|
box-shadow: 0 0 10px var(--proto-cyan);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
/* Base theme colors */
|
/* Base theme colors */
|
||||||
:root {
|
:root {
|
||||||
/* Main colors */
|
/* Main colors */
|
||||||
--background-primary: #1a0b2e;
|
--background-primary: #1a0b2e;
|
||||||
--background-secondary: #2f1c54;
|
--background-secondary: #2f1c54;
|
||||||
--accent-primary: #9d4edd;
|
--accent-primary: #9d4edd;
|
||||||
--accent-neon: #b249f8;
|
--accent-neon: #b249f8;
|
||||||
--text-glow: #e0aaff;
|
--text-glow: #e0aaff;
|
||||||
--text-primary: #ffffff;
|
--text-primary: #ffffff;
|
||||||
--dark-accent: #240046;
|
--dark-accent: #240046;
|
||||||
/* Fox theme accents */
|
/* Fox theme accents */
|
||||||
--fox-pink: #ffc6e5;
|
--fox-pink: #ffc6e5;
|
||||||
--fox-pink-glow: #ffadd6;
|
--fox-pink-glow: #ffadd6;
|
||||||
--fox-orange: #ff9466;
|
--fox-orange: #ff9466;
|
||||||
--fox-white: #fff5f9;
|
--fox-white: #fff5f9;
|
||||||
/* Gradients */
|
/* Gradients */
|
||||||
--gradient-primary: linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%);
|
--gradient-primary: linear-gradient(
|
||||||
--gradient-card: linear-gradient(135deg, rgba(47, 28, 84, 0.3) 0%, rgba(157, 78, 221, 0.1) 100%);
|
135deg,
|
||||||
--gradient-hover: linear-gradient(135deg, rgba(157, 78, 221, 0.2) 0%, rgba(178, 73, 248, 0.1) 100%);
|
var(--background-primary) 0%,
|
||||||
|
var(--background-secondary) 100%
|
||||||
|
);
|
||||||
|
--gradient-card: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(47, 28, 84, 0.3) 0%,
|
||||||
|
rgba(157, 78, 221, 0.1) 100%
|
||||||
|
);
|
||||||
|
--gradient-hover: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(157, 78, 221, 0.2) 0%,
|
||||||
|
rgba(178, 73, 248, 0.1) 100%
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,79 @@
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
/* Animated background */
|
/* Animated background */
|
||||||
.animated-bg {
|
.animated-bg {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animated-bg::before {
|
.animated-bg::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background: var(--gradient-primary);
|
background: var(--gradient-primary);
|
||||||
background-size: 200% 200%;
|
background-size: 200% 200%;
|
||||||
animation: gradientShift 15s ease infinite;
|
animation: gradientShift 15s ease infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fox ear styling */
|
/* Fox ear styling */
|
||||||
.fox-ear {
|
.fox-ear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
background-color: var(--fox-pink);
|
background-color: var(--fox-pink);
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-ear-left {
|
.fox-ear-left {
|
||||||
top: -15px;
|
top: -15px;
|
||||||
left: -15px;
|
left: -15px;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
border-radius: 0 0 0 15px;
|
border-radius: 0 0 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fox-ear-right {
|
.fox-ear-right {
|
||||||
top: -15px;
|
top: -15px;
|
||||||
right: -15px;
|
right: -15px;
|
||||||
transform: rotate(-45deg);
|
transform: rotate(-45deg);
|
||||||
border-radius: 0 0 15px 0;
|
border-radius: 0 0 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover effects */
|
/* Hover effects */
|
||||||
.hover-glow {
|
.hover-glow {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-glow:hover {
|
.hover-glow:hover {
|
||||||
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
|
filter: drop-shadow(0 0 4px var(--fox-pink-glow));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Spacing utilities */
|
/* Spacing utilities */
|
||||||
.section-spacing {
|
.section-spacing {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-spacing > * + * {
|
.section-spacing > * + * {
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-spacing {
|
.content-spacing {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-spacing > * + * {
|
.content-spacing > * + * {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradientShift {
|
@keyframes gradientShift {
|
||||||
0%, 100% {
|
0%,
|
||||||
background-position: 0% 50%;
|
100% {
|
||||||
}
|
background-position: 0% 50%;
|
||||||
50% {
|
}
|
||||||
background-position: 100% 50%;
|
50% {
|
||||||
}
|
background-position: 100% 50%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,57 @@
|
||||||
export interface Position {
|
export interface Position {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PowerUp {
|
export interface PowerUp {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'SPEED' | 'SHIELD' | 'MAGNET';
|
type: "SPEED" | "SHIELD" | "MAGNET";
|
||||||
duration: number;
|
duration: number;
|
||||||
position: Position;
|
position: Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Collectible {
|
export interface Collectible {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'STAR' | 'GEM' | 'KEY';
|
type: "STAR" | "GEM" | "KEY";
|
||||||
value: number;
|
value: number;
|
||||||
position: Position;
|
position: Position;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Enemy {
|
export interface Enemy {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'WOLF' | 'OWL' | 'HUNTER';
|
type: "WOLF" | "OWL" | "HUNTER";
|
||||||
position: Position;
|
position: Position;
|
||||||
direction: Position;
|
direction: Position;
|
||||||
speed: number;
|
speed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
position: Position;
|
position: Position;
|
||||||
health: number;
|
health: number;
|
||||||
speed: number;
|
speed: number;
|
||||||
powerUps: PowerUp[];
|
powerUps: PowerUp[];
|
||||||
isInvincible: boolean;
|
isInvincible: boolean;
|
||||||
hasKey: boolean;
|
hasKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
player: PlayerState;
|
player: PlayerState;
|
||||||
enemies: Enemy[];
|
enemies: Enemy[];
|
||||||
collectibles: Collectible[];
|
collectibles: Collectible[];
|
||||||
powerUps: PowerUp[];
|
powerUps: PowerUp[];
|
||||||
score: number;
|
score: number;
|
||||||
level: number;
|
level: number;
|
||||||
gameStatus: 'MENU' | 'PLAYING' | 'PAUSED' | 'GAME_OVER';
|
gameStatus: "MENU" | "PLAYING" | "PAUSED" | "GAME_OVER";
|
||||||
highScores: number[];
|
highScores: number[];
|
||||||
timePlayed: number;
|
timePlayed: number;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
movePlayer: (direction: Position) => void;
|
movePlayer: (direction: Position) => void;
|
||||||
updateEnemies: (deltaTime?: number) => void;
|
updateEnemies: (deltaTime?: number) => void;
|
||||||
collectItem: (itemId: string) => void;
|
collectItem: (itemId: string) => void;
|
||||||
takeDamage: (amount: number) => void;
|
takeDamage: (amount: number) => void;
|
||||||
activatePowerUp: (powerUpId: string) => void;
|
activatePowerUp: (powerUpId: string) => void;
|
||||||
startNewGame: () => void;
|
startNewGame: () => void;
|
||||||
pauseGame: () => void;
|
pauseGame: () => void;
|
||||||
resumeGame: () => void;
|
resumeGame: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export interface Track {
|
export interface Track {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ export function calculatePreciseAge(birthDate: Date): number {
|
||||||
const diffTime = today.getTime() - birthDate.getTime();
|
const diffTime = today.getTime() - birthDate.getTime();
|
||||||
const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25);
|
const diffYears = diffTime / (1000 * 60 * 60 * 24 * 365.25);
|
||||||
return Number(diffYears.toFixed(8));
|
return Number(diffYears.toFixed(8));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,39 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from 'path'
|
import babel from 'vite-plugin-babel';
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
resolve: {
|
react(),
|
||||||
alias: {
|
babel({
|
||||||
'@': path.resolve(__dirname, './src'),
|
babelConfig: {
|
||||||
},
|
plugins: ['babel-plugin-react-compiler'],
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: 3000,
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:8787',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
},
|
||||||
},
|
}),],
|
||||||
},
|
resolve: {
|
||||||
build: {
|
alias: {
|
||||||
outDir: 'dist',
|
"@": path.resolve(__dirname, "./src"),
|
||||||
sourcemap: true,
|
|
||||||
rollupOptions: {
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
server: {
|
||||||
})
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8787",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
"react-vendor": ["react", "react-dom", "react-router-dom"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue