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