Formating and small updates

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

View file

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

View file

@ -1,23 +1,35 @@
<!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
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
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 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: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>
@ -25,4 +37,3 @@
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

4635
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -4,18 +4,17 @@ 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 = () => {
@ -24,18 +23,19 @@ const useEndOSAnimation = () => {
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');
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';
const sessionSeen =
sessionStorage.getItem("endos-boot-complete") === "true";
return skipParam === 'true' || sessionSeen;
return skipParam === "true" || sessionSeen;
});
// Handle boot animation completion
const handleBootComplete = () => {
setBootComplete(true);
sessionStorage.setItem('endos-boot-complete', 'true');
sessionStorage.setItem("endos-boot-complete", "true");
};
return { bootComplete, skipBoot, handleBootComplete };
@ -47,13 +47,15 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
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 [_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);
setIsStatusVisible((prev) => !prev);
};
// Simulate random switches for demo purposes
@ -61,20 +63,21 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
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 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
setNotificationType("switch");
setSelectedAlter(""); // Random alter will be selected
} else if (rand < 0.9) {
setNotificationType('notice');
setNotificationMessage('System communication active');
setNotificationType("notice");
setNotificationMessage("System communication active");
} else {
setNotificationType('warning');
setNotificationMessage('System experiencing stress');
setNotificationType("warning");
setNotificationMessage("System experiencing stress");
}
setShowNotification(true);
@ -92,7 +95,9 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
<>
<div
className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
isStatusVisible ? 'translate-y-0' : 'translate-y-[calc(100%-40px)]'
isStatusVisible
? "translate-y-0"
: "translate-y-[calc(100%-40px)]"
}`}
>
<div
@ -100,7 +105,9 @@ const AuthChecker = ({ children }: { children: React.ReactNode }) => {
onClick={toggleStatus}
>
<span className="text-xs font-medium">
{isStatusVisible ? "Hide System Status" : "System Status"}
{isStatusVisible
? "Hide System Status"
: "System Status"}
</span>
</div>
{/* <SystemStatus
@ -147,11 +154,16 @@ const App = () => {
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;
@ -164,7 +176,7 @@ const App = () => {
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,10 +184,10 @@ 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 (
@ -186,14 +198,18 @@ const App = () => {
)}
{/* Main Application - Only visible after boot animation completes */}
<div style={{
visibility: bootComplete || skipBoot ? 'visible' : 'hidden',
<div
style={{
visibility: bootComplete || skipBoot ? "visible" : "hidden",
opacity: bootComplete || skipBoot ? 1 : 0,
transition: 'opacity 0.5s ease-in-out'
}}>
transition: "opacity 0.5s ease-in-out",
}}
>
<Router>
<AuthChecker>
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}>
<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">
@ -216,10 +232,15 @@ const App = () => {
<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="/"
element={<AboutPage />}
/>
<Route
path="/projects"
element={<ProjectsPage />}
/>
{/* <Route path="/apcsp" element={<APCSPPage />} /> */}
{/* <Route path="/login" element={<LoginPage />} />
<Route
path="/system"
@ -229,28 +250,40 @@ const App = () => {
</ProtectedRoute>
}
/> */}
<Route path="*" element={
<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>
<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>
<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>
<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
v0.9.5
</div>
</div>
</footer>
</div>
{/* Fox Game Overlay - Activated by Konami Code */}
{isGameActive && (
<>

View file

@ -1,5 +1,5 @@
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
@ -18,7 +18,7 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
skipAnimation = false,
// Rename with underscores to indicate variables are intentionally unused
_customLogo,
_customColors
_customColors,
}) => {
const [active, setActive] = useState(true);
const [bootStage, setBootStage] = useState(0);
@ -41,7 +41,7 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
{ 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
{ stage: 8, delay: 2000 }, // Fade out
];
let timeout: any; // Using any instead of NodeJS.Timeout
@ -52,20 +52,20 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
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');
document.querySelectorAll(".boot-stage").forEach((el) => {
el.classList.remove("active");
});
// Reset content visibility
document.querySelectorAll('.boot-content').forEach(el => {
el.classList.remove('active');
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');
document.querySelectorAll(".boot-content").forEach((el) => {
el.classList.add("active");
});
// Then activate the correct stage
@ -127,34 +127,62 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
{/* Boot Sequence Content */}
<div className={`boot-content`}>
{/* BIOS Check */}
<div className={`boot-stage bios ${bootStage === 2 ? 'active' : ''}`}>
<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 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={`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 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={`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="module-name">
ProtogenCore
</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
@ -162,7 +190,9 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
</div>
<div className="module-item">
<div className="module-icon"></div>
<div className="module-name">VisorDisplay</div>
<div className="module-name">
VisorDisplay
</div>
</div>
<div className="module-item">
<div className="module-icon"></div>
@ -172,29 +202,53 @@ const EndOSBootAnimation: React.FC<EndOSBootAnimationProps> = ({
</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
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={`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 className="logo-subtitle">
ProtoFox Operating System
</div>
</div>
{/* System Ready */}
<div className={`boot-stage system-ready ${bootStage === 7 && bootComplete ? 'active' : ''}`}>
<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 className="welcome-message">
Welcome back, ProtoFox
</div>
<div className="boot-complete-message">
EndOS v1.0 is fully operational
</div>
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,18 @@
import { useState, useEffect } from 'react';
import { Music } from 'lucide-react';
import { useState, useEffect } from "react";
import { Music } from "lucide-react";
interface LastFMImage {
'#text': string;
"#text": string;
size: string;
}
interface LastFMTrack {
name: string;
artist: {
'#text': string;
"#text": string;
};
image: LastFMImage[];
'@attr'?: {
"@attr"?: {
nowplaying: string;
};
}
@ -43,7 +43,9 @@ const MusicDisplay = () => {
const USERNAME = import.meta.env.VITE_LASTFM_USERNAME;
if (!API_KEY || !USERNAME) {
throw new Error('Last.fm API key or username not configured');
throw new Error(
"Last.fm API key or username not configured",
);
}
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${USERNAME}&api_key=${API_KEY}&format=json&limit=1`;
@ -63,12 +65,17 @@ const MusicDisplay = () => {
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';
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";
const isCurrentlyPlaying = track['@attr']?.nowplaying === 'true';
const isCurrentlyPlaying =
track["@attr"]?.nowplaying === "true";
if (!isCurrentlyPlaying) {
setCurrentTrack(null);
@ -78,15 +85,18 @@ const MusicDisplay = () => {
setCurrentTrack({
name: track.name,
artist: track.artist['#text'],
artist: track.artist["#text"],
image: imageUrl,
isPlaying: true
isPlaying: true,
});
setExpanded(false);
setError(null);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'An error occurred';
console.error('Last.fm error:', error);
const errorMessage =
error instanceof Error
? error.message
: "An error occurred";
console.error("Last.fm error:", error);
setError(errorMessage);
} finally {
setLoading(false);
@ -117,10 +127,12 @@ const MusicDisplay = () => {
if (!currentTrack) {
return (
<div className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? 'h-[352px]' : 'h-[88px]'}`}>
<div
className={`overflow-hidden transition-[height] duration-500 ease-in-out ${expanded ? "h-[352px]" : "h-[88px]"}`}
>
<iframe
title="Spotify Playlist"
style={{ borderRadius: '12px' }}
style={{ borderRadius: "12px" }}
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
width="100%"
height="352"

View file

@ -1,8 +1,11 @@
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">
@ -10,7 +13,7 @@ const Navbar = () => {
<div className="nav-links">
<Link
to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
>
<Home size={20} />
<span>About</span>
@ -18,27 +21,35 @@ const Navbar = () => {
<Link
to="/projects"
className={`nav-link ${location.pathname === '/projects' ? 'active' : ''}`}
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
href="https://twitch.tv/EndofTimee"
@ -52,11 +63,11 @@ const Navbar = () => {
<div className="nav-link">
<iframe
src="https://github.com/sponsors/EndofTimee/button"
title="Sponsor EndofTimee"
src="https://github.com/sponsors/System-End/button"
title="Sponsor End!"
height="32"
width="114"
style={{ border: 0, borderRadius: '6px' }}
style={{ border: 0, borderRadius: "6px" }}
></iframe>
</div>
</div>

View file

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

View file

@ -5,7 +5,7 @@ const SpotifyEmbed = () => {
src="https://open.spotify.com/embed/playlist/58ggvvTcs95yhcSeSxLGks?utm_source=generator"
width="100%"
height="100%"
style={{ borderRadius: '12px' }}
style={{ borderRadius: "12px" }}
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
@ -13,6 +13,6 @@ const SpotifyEmbed = () => {
/>
</div>
);
};
};
export default SpotifyEmbed;
export default SpotifyEmbed;

View file

@ -94,8 +94,8 @@ 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 {

View file

@ -1,24 +1,24 @@
import React, { useState, useEffect } from 'react';
import './ThemeToggle.css';
import React, { useState, useEffect } from "react";
import "./ThemeToggle.css";
interface ThemeToggleProps {
className?: string;
}
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => {
const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = "" }) => {
const [isProtoFoxMode, setIsProtoFoxMode] = useState(() => {
// Check if user had previously selected protofox mode
return localStorage.getItem('theme') === 'protofox';
return localStorage.getItem("theme") === "protofox";
});
// Apply theme class to body when toggle changes
useEffect(() => {
if (isProtoFoxMode) {
document.body.classList.add('protofox-theme');
localStorage.setItem('theme', 'protofox');
document.body.classList.add("protofox-theme");
localStorage.setItem("theme", "protofox");
} else {
document.body.classList.remove('protofox-theme');
localStorage.setItem('theme', 'default');
document.body.classList.remove("protofox-theme");
localStorage.setItem("theme", "default");
}
}, [isProtoFoxMode]);
@ -32,13 +32,17 @@ const ThemeToggle: React.FC<ThemeToggleProps> = ({ className = '' }) => {
/>
<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={`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'}
{isProtoFoxMode ? "ProtoFox Mode" : "Standard Mode"}
</span>
</label>
</div>

View file

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

View file

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

View file

@ -1,4 +1,10 @@
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
import {
createContext,
useContext,
useState,
ReactNode,
useEffect,
} from "react";
interface SystemMember {
id: string;
@ -8,9 +14,16 @@ interface SystemMember {
}
interface SystemState {
safetyLevel: 'safe' | 'unsafe' | 'sorta-safe' | 'unknown';
mentalState: 'ok' | 'bad' | 'very-bad' | 'panic' | 'spiraling' | 'unstable' | 'delusional';
frontingStatus: 'single' | 'co-fronting' | 'switching' | 'unknown';
safetyLevel: "safe" | "unsafe" | "sorta-safe" | "unknown";
mentalState:
| "ok"
| "bad"
| "very-bad"
| "panic"
| "spiraling"
| "unstable"
| "delusional";
frontingStatus: "single" | "co-fronting" | "switching" | "unknown";
currentFronters: SystemMember[];
}
@ -25,13 +38,13 @@ interface AuthContextType {
// 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
@ -40,57 +53,58 @@ 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';
const stored = localStorage.getItem("isAuthenticated");
return stored === "true";
});
const [username, setUsername] = useState<string | null>(() => {
return localStorage.getItem('username');
return localStorage.getItem("username");
});
// Initialize system state from localStorage or set defaults
const [systemState, setSystemState] = useState<SystemState | null>(() => {
const stored = localStorage.getItem('systemState');
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;
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());
localStorage.setItem("isAuthenticated", isAuthenticated.toString());
if (username) {
localStorage.setItem('username', username);
localStorage.setItem("username", username);
} else {
localStorage.removeItem('username');
localStorage.removeItem("username");
}
// If logged out, clear system state
if (!isAuthenticated) {
localStorage.removeItem('systemState');
localStorage.removeItem("systemState");
setSystemState(null);
} else if (systemState) {
localStorage.setItem('systemState', JSON.stringify(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));
await new Promise((resolve) => setTimeout(resolve, 800));
// We use credential verification with multiple allowed passwords for different contexts
const validCredentials = [
{ user: 'system', pass: '.' },
];
const validCredentials = [{ user: "system", pass: "." }];
const isValid = validCredentials.some(
cred => cred.user === username.toLowerCase() && cred.pass === password
(cred) =>
cred.user === username.toLowerCase() && cred.pass === password,
);
if (isValid) {
@ -99,14 +113,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
// Initialize system state on login
const initialState: SystemState = {
safetyLevel: 'safe',
mentalState: 'ok',
frontingStatus: 'single',
currentFronters: [systemMembers[0]]
safetyLevel: "safe",
mentalState: "ok",
frontingStatus: "single",
currentFronters: [systemMembers[0]],
};
setSystemState(initialState);
localStorage.setItem('systemState', JSON.stringify(initialState));
localStorage.setItem("systemState", JSON.stringify(initialState));
return true;
}
@ -122,7 +136,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
setSystemState(null);
// Clear sensitive data from localStorage
localStorage.removeItem('systemState');
localStorage.removeItem("systemState");
}, 300);
};
@ -131,7 +145,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const updatedState = { ...systemState, ...newState };
setSystemState(updatedState);
localStorage.setItem('systemState', JSON.stringify(updatedState));
localStorage.setItem("systemState", JSON.stringify(updatedState));
};
// Construct the context value
@ -141,7 +155,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
systemState,
login,
logout,
updateSystemState
updateSystemState,
};
return (
@ -155,7 +169,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

10
src/env.d.ts vendored
View file

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

View file

@ -1,10 +1,10 @@
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);
@ -17,11 +17,16 @@ const FoxGame: React.FC = () => {
// Konami code activation
useEffect(() => {
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;
@ -37,8 +42,8 @@ const FoxGame: React.FC = () => {
}
};
window.addEventListener('keydown', handleKeydown);
return () => window.removeEventListener('keydown', handleKeydown);
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}, []);
if (!isActive) return null;
@ -51,39 +56,41 @@ const FoxGame: React.FC = () => {
<Player />
{/* Render collectibles */}
{gameStore.collectibles.map(collectible => (
{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'
collectible.type === "GEM"
? "bg-purple-500"
: "bg-yellow-400"
}`}
style={{
left: `${collectible.position.x}%`,
top: `${collectible.position.y}%`
top: `${collectible.position.y}%`,
}}
/>
))}
{/* Render enemies */}
{gameStore.enemies.map(enemy => (
{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}%`
top: `${enemy.position.y}%`,
}}
/>
))}
{/* Render power-ups */}
{gameStore.powerUps.map(powerUp => (
{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}%`
top: `${powerUp.position.y}%`,
}}
/>
))}

View file

@ -1,6 +1,6 @@
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();
@ -8,7 +8,7 @@ export const GameHUD: React.FC = () => {
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 `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
};
return (
@ -19,7 +19,9 @@ export const GameHUD: React.FC = () => {
<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'
player.health > 20
? "text-red-500"
: "text-red-500 animate-pulse"
}`}
/>
<div className="w-32 h-3 bg-background-secondary rounded-full overflow-hidden">
@ -48,10 +50,14 @@ export const GameHUD: React.FC = () => {
{/* 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="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>
<span className="text-xl">
{score.toLocaleString()}
</span>
</div>
</div>
</div>
@ -64,7 +70,12 @@ export const GameHUD: React.FC = () => {
</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>
<span>
Best:{" "}
{Math.max(
...useGameStore.getState().highScores,
).toLocaleString()}
</span>
</div>
</div>
</div>

View file

@ -1,19 +1,24 @@
// 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();
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' && (
{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>
<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">
@ -34,9 +39,11 @@ export const GameOverlay: React.FC = () => {
</div>
)}
{gameStatus === 'PAUSED' && (
{gameStatus === "PAUSED" && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-glow">Game Paused</h2>
<h2 className="text-3xl font-bold text-glow">
Game Paused
</h2>
<div className="flex justify-center gap-4">
<button
onClick={resumeGame}
@ -56,12 +63,16 @@ export const GameOverlay: React.FC = () => {
</div>
)}
{gameStatus === 'GAME_OVER' && (
{gameStatus === "GAME_OVER" && (
<div className="text-center space-y-6">
<h2 className="text-3xl font-bold text-red-500">Game Over</h2>
<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>
<p className="text-4xl font-bold text-accent-neon">
{score.toLocaleString()}
</p>
</div>
<button
onClick={startNewGame}

View file

@ -1,18 +1,18 @@
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' : ''
player.isInvincible ? "animate-pulse" : ""
}`}
style={{
left: `${player.position.x}%`,
top: `${player.position.y}%`,
transform: 'translate(-50%, -50%)'
transform: "translate(-50%, -50%)",
}}
>
{/* Fox body */}

View file

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import useGameStore from '../state/gameStore';
import { useEffect } from "react";
import useGameStore from "../state/gameStore";
export const useGameControls = () => {
const gameStore = useGameStore();
@ -10,10 +10,10 @@ export const useGameControls = () => {
const handleKeyDown = (e: KeyboardEvent) => {
keys.add(e.key);
if (e.key === 'Escape') {
if (gameStore.gameStatus === 'PLAYING') {
if (e.key === "Escape") {
if (gameStore.gameStatus === "PLAYING") {
gameStore.pauseGame();
} else if (gameStore.gameStatus === 'PAUSED') {
} else if (gameStore.gameStatus === "PAUSED") {
gameStore.resumeGame();
}
}
@ -24,18 +24,20 @@ export const useGameControls = () => {
};
const updatePlayerMovement = () => {
if (gameStore.gameStatus !== 'PLAYING') return;
if (gameStore.gameStatus !== "PLAYING") return;
const direction = { x: 0, y: 0 };
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;
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;
if (direction.x !== 0 || direction.y !== 0) {
// Normalize diagonal movement
const magnitude = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
const magnitude = Math.sqrt(
direction.x * direction.x + direction.y * direction.y,
);
direction.x /= magnitude;
direction.y /= magnitude;
@ -50,12 +52,12 @@ export const useGameControls = () => {
};
animationFrameId = requestAnimationFrame(gameLoop);
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
cancelAnimationFrame(animationFrameId);
};
}, []);

View file

@ -1,5 +1,5 @@
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>();
@ -15,12 +15,12 @@ export const useGameLoop = () => {
lastSpawnRef.current = now;
const collectible: any = {
id: `collectible-${now}`,
type: Math.random() > 0.8 ? 'GEM' : 'STAR',
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
}
y: Math.random() * 90 + 5,
},
};
gameStore.collectibles.push(collectible);
@ -30,7 +30,7 @@ export const useGameLoop = () => {
if (!lastUpdateRef.current) lastUpdateRef.current = timestamp;
const deltaTime = timestamp - lastUpdateRef.current;
if (gameStore.gameStatus === 'PLAYING') {
if (gameStore.gameStatus === "PLAYING") {
// Update entities
gameStore.updateEnemies(deltaTime);
@ -41,7 +41,7 @@ export const useGameLoop = () => {
const { player, enemies, collectibles } = gameStore;
// Enemy collisions
enemies.forEach(enemy => {
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);
@ -52,7 +52,7 @@ export const useGameLoop = () => {
});
// Collectible collisions
collectibles.forEach(collectible => {
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);
@ -76,4 +76,3 @@ export const useGameLoop = () => {
};
}, []);
};

View file

@ -1,5 +1,5 @@
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: {
@ -8,14 +8,14 @@ const useGameStore = create<GameState>((set, get) => ({
speed: 5,
powerUps: [],
isInvincible: false,
hasKey: false
hasKey: false,
},
enemies: [],
collectibles: [],
powerUps: [],
score: 0,
level: 1,
gameStatus: 'MENU',
gameStatus: "MENU",
highScores: [],
timePlayed: 0,
@ -25,16 +25,28 @@ const useGameStore = create<GameState>((set, get) => ({
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))
}
}
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 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);
@ -43,12 +55,12 @@ const useGameStore = create<GameState>((set, get) => ({
...enemy,
direction: {
x: dx / distance,
y: dy / distance
y: dy / distance,
},
position: {
x: enemy.position.x + (enemy.direction.x * enemy.speed),
y: enemy.position.y + (enemy.direction.y * enemy.speed)
}
x: enemy.position.x + enemy.direction.x * enemy.speed,
y: enemy.position.y + enemy.direction.y * enemy.speed,
},
};
});
@ -57,12 +69,12 @@ const useGameStore = create<GameState>((set, get) => ({
collectItem: (itemId: string) => {
const { collectibles, score } = get();
const item = collectibles.find(c => c.id === itemId);
const item = collectibles.find((c) => c.id === itemId);
if (!item) return;
set({
collectibles: collectibles.filter(c => c.id !== itemId),
score: score + item.value
collectibles: collectibles.filter((c) => c.id !== itemId),
score: score + item.value,
});
},
@ -74,23 +86,23 @@ const useGameStore = create<GameState>((set, get) => ({
set({
player: {
...player,
health: newHealth
health: newHealth,
},
gameStatus: newHealth <= 0 ? 'GAME_OVER' : gameStatus
gameStatus: newHealth <= 0 ? "GAME_OVER" : gameStatus,
});
},
activatePowerUp: (powerUpId: string) => {
const { player, powerUps } = get();
const powerUp = powerUps.find(p => p.id === powerUpId);
const powerUp = powerUps.find((p) => p.id === powerUpId);
if (!powerUp) return;
set({
player: {
...player,
powerUps: [...player.powerUps, powerUp]
powerUps: [...player.powerUps, powerUp],
},
powerUps: powerUps.filter(p => p.id !== powerUpId)
powerUps: powerUps.filter((p) => p.id !== powerUpId),
});
setTimeout(() => {
@ -98,32 +110,35 @@ const useGameStore = create<GameState>((set, get) => ({
set({
player: {
...currentPlayer,
powerUps: currentPlayer.powerUps.filter(p => p.id !== powerUp.id)
}
powerUps: currentPlayer.powerUps.filter(
(p) => p.id !== powerUp.id,
),
},
});
}, powerUp.duration);
},
startNewGame: () => set({
startNewGame: () =>
set({
player: {
position: { x: 50, y: 50 },
health: 100,
speed: 5,
powerUps: [],
isInvincible: false,
hasKey: false
hasKey: false,
},
enemies: [],
collectibles: [],
powerUps: [],
score: 0,
level: 1,
gameStatus: 'PLAYING',
timePlayed: 0
gameStatus: "PLAYING",
timePlayed: 0,
}),
pauseGame: () => set({ gameStatus: 'PAUSED' }),
resumeGame: () => set({ gameStatus: 'PLAYING' })
pauseGame: () => set({ gameStatus: "PAUSED" }),
resumeGame: () => set({ gameStatus: "PLAYING" }),
}));
export default useGameStore;

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import type { GithubRepo } from '@/types';
import { useState, useEffect } from "react";
import type { GithubRepo } from "@/types";
const useGithubRepos = () => {
const [repos, setRepos] = useState<GithubRepo[]>([]);
@ -9,29 +9,40 @@ 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);
@ -39,7 +50,7 @@ const useGithubRepos = () => {
} catch (err) {
const error = err as Error;
setError(error.message);
console.error('Error fetching repos:', err);
console.error("Error fetching repos:", err);
} finally {
setLoading(false);
}

View file

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

View file

@ -1,12 +1,14 @@
import FoxCard from '@/components/FoxCard';
import { Code, Cpu } from 'lucide-react';
import FoxCard from "@/components/FoxCard";
import { Code, Cpu } from "lucide-react";
const APCSPPage = () => {
return (
<div className="page-container">
<FoxCard className="header-card">
<h1 className="text-glow">AP Computer Science Principles</h1>
<p className="text-gradient">Exploring the foundations of modern computing</p>
<p className="text-gradient">
Exploring the foundations of modern computing
</p>
</FoxCard>
<div className="content-grid">
@ -15,7 +17,10 @@ const APCSPPage = () => {
<Code size={24} className="text-accent-primary" />
<h2>Programming Concepts</h2>
</div>
<p>Learn the creative aspects of programming, abstractions, and algorithms</p>
<p>
Learn the creative aspects of programming, abstractions,
and algorithms
</p>
</FoxCard>
<FoxCard>

View file

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

View file

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

View file

@ -1,14 +1,16 @@
const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => {
if (onPerfEntry && typeof onPerfEntry === 'function') {
import('web-vitals').then((vitals) => {
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);
})
.catch((error) => {
console.error("Error loading web-vitals:", error);
});
}
};

View file

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

View file

@ -1,4 +1,3 @@
/* Full screen overlay */
.endos-boot-container {
position: fixed;
@ -8,8 +7,8 @@
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', monospace;
color: #00E5FF;
font-family: "Orbitron", monospace;
color: #00e5ff;
overflow: hidden;
}
@ -19,18 +18,24 @@
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg,
background: linear-gradient(
90deg,
rgba(0, 229, 255, 0) 0%,
rgba(0, 229, 255, 0.8) 50%,
rgba(0, 229, 255, 0) 100%);
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 */
@ -61,13 +66,23 @@
}
@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 */
@ -77,7 +92,7 @@
max-width: 800px;
height: 60vh;
max-height: 600px;
border: 2px solid #00E5FF;
border: 2px solid #00e5ff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5);
overflow: hidden;
@ -101,8 +116,8 @@
left: 0;
width: 100%;
height: 3px;
background-color: #00E5FF;
box-shadow: 0 0 10px #00E5FF;
background-color: #00e5ff;
box-shadow: 0 0 10px #00e5ff;
animation: glowPulse 2s ease-in-out infinite;
}
@ -115,8 +130,19 @@
}
@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 */
@ -143,13 +169,20 @@
transform: translate(-50%, -50%);
margin: 0 auto;
padding: 20px;
background-color: rgba(0, 0, 0, 0.6); /* Darker background to prevent seeing through */
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;
transition:
opacity 0.3s ease,
visibility 0.3s ease;
max-height: 80%;
overflow-y: auto;
z-index: 1; /* Ensure z-index is consistent */
@ -179,13 +212,13 @@
.bios-header {
font-size: 24px;
margin-bottom: 20px;
color: #FF005C;
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-family: "Courier New", monospace;
font-size: 14px;
margin: 8px 0;
color: #8be9fd;
@ -195,15 +228,19 @@
}
@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;
color: #ff005c;
text-align: center;
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
}
@ -220,18 +257,22 @@
.scan-progress-bar {
height: 100%;
background: linear-gradient(90deg, #00E5FF, #FF005C);
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-family: "Courier New", monospace;
font-size: 14px;
margin: 8px 0;
color: #8be9fd;
@ -241,7 +282,7 @@
.module-header {
font-size: 20px;
margin-bottom: 15px;
color: #FF005C;
color: #ff005c;
text-align: center;
text-shadow: 0 0 10px rgba(255, 0, 92, 0.7);
}
@ -264,28 +305,49 @@
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;
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 {
@ -303,7 +365,7 @@
}
.fox-trait {
font-family: 'Courier New', monospace;
font-family: "Courier New", monospace;
font-size: 14px;
margin: 10px 0;
color: #ff9466;
@ -313,19 +375,35 @@
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: '>';
content: ">";
position: absolute;
left: 0;
color: #ff9466;
@ -344,7 +422,7 @@
font-weight: bold;
margin-bottom: 10px;
text-align: center;
background: linear-gradient(90deg, #ff9466, #00E5FF);
background: linear-gradient(90deg, #ff9466, #00e5ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
@ -359,7 +437,7 @@
}
.logo-os {
color: #00E5FF;
color: #00e5ff;
}
.logo-subtitle {
@ -370,8 +448,14 @@
}
@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 */
@ -382,13 +466,18 @@
.ready-status {
font-size: 28px;
margin-bottom: 15px;
color: #00E5FF;
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 {
@ -414,7 +503,7 @@
border: 1px solid rgba(255, 0, 92, 0.5);
border-radius: 5px;
padding: 10px 20px; /* Larger size */
font-family: 'Orbitron', sans-serif;
font-family: "Orbitron", sans-serif;
font-size: 14px; /* Larger font */
font-weight: bold;
cursor: pointer;
@ -433,8 +522,13 @@
}
@keyframes skipPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* Accessibility */
@ -457,7 +551,8 @@
height: 80vh;
}
.visor-left-ear, .visor-right-ear {
.visor-left-ear,
.visor-right-ear {
width: 30px;
height: 30px;
top: -15px;
@ -484,7 +579,10 @@
grid-template-columns: 1fr;
}
.bios-header, .scan-header, .module-header, .fox-header {
.bios-header,
.scan-header,
.module-header,
.fox-header {
font-size: 18px;
}
@ -508,11 +606,16 @@
padding: 10px;
}
.bios-header, .scan-header, .module-header, .fox-header {
.bios-header,
.scan-header,
.module-header,
.fox-header {
font-size: 16px;
}
.boot-text-line, .scan-detail, .fox-trait {
.boot-text-line,
.scan-detail,
.fox-trait {
font-size: 12px;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,68 +1,147 @@
/* 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 */

View file

@ -74,7 +74,11 @@
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%);
background: linear-gradient(
135deg,
rgba(47, 28, 84, 0.2) 0%,
rgba(157, 78, 221, 0.05) 100%
);
}
.fox-card:hover {

View file

@ -1,6 +1,8 @@
/* Default cursor for all elements */
* {
cursor: url('@/assets/cursors/default.svg') 16 16, auto;
cursor:
url("@/assets/cursors/default.svg") 16 16,
auto;
}
/* Interactive elements cursor */
@ -12,14 +14,18 @@ input[type="button"],
select,
.interactive,
.nav-link {
cursor: url('@/assets/cursors/paw.svg') 16 16, pointer;
cursor:
url("@/assets/cursors/paw.svg") 16 16,
pointer;
}
/* Loading state cursor */
.loading,
:disabled,
[aria-busy="true"] {
cursor: url('@/assets/cursors/tail-loading.svg') 16 16, progress;
cursor:
url("@/assets/cursors/tail-loading.svg") 16 16,
progress;
}
/* Hover effects for interactive elements */

View file

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

View file

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

View file

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

View file

@ -1,27 +1,27 @@
/* 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");
body.protofox-theme {
/* Override core variables with protofox colors */
--background-primary: #1a0b2e;
--background-secondary: #2f1c54;
--accent-primary: #FF7E5F;
--accent-secondary: #00E5FF;
--accent-primary: #ff7e5f;
--accent-secondary: #00e5ff;
--text-glow: #4dffc7;
--visor-glow: #FF005C;
--visor-glow: #ff005c;
--fox-orange: #ff9466;
--fox-orange-dark: #e07242;
--proto-blue: #4dc3ff;
--proto-cyan: #00e5ff;
--proto-neon: #39FFBF;
--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;
--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');
body.protofox-theme h1,
body.protofox-theme h2,
body.protofox-theme h3,
@ -38,7 +38,7 @@ body.protofox-theme {
/* Add circuit background only in protofox mode */
body.protofox-theme::before {
content: '';
content: "";
position: fixed;
inset: 0;
background-color: var(--background-primary);
@ -46,7 +46,10 @@ body.protofox-theme::before {
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-size:
30px 30px,
30px 30px,
30px 30px;
background-position: 0 0;
opacity: 0.15;
z-index: -1;
@ -55,19 +58,23 @@ body.protofox-theme::before {
/* 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: '';
content: "";
position: fixed;
top: -10px;
left: 0;
@ -82,8 +89,12 @@ body.protofox-theme::after {
}
@keyframes protofox-scan {
0% { top: -10px; }
100% { top: 100vh; }
0% {
top: -10px;
}
100% {
top: 100vh;
}
}
/* Enhance existing components in protofox mode */
@ -97,17 +108,17 @@ body.protofox-theme .fox-card {
}
body.protofox-theme .fox-card::before {
content: '';
content: "";
position: absolute;
inset: 0;
background: url('/images/protofox/fur-texture.png');
background: url("/images/protofox/fur-texture.png");
opacity: 0.04;
mix-blend-mode: overlay;
pointer-events: none;
}
body.protofox-theme .fox-card::after {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -127,7 +138,7 @@ body.protofox-theme .button {
body.protofox-theme button:not(.theme-toggle *):before,
body.protofox-theme .button:before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -150,7 +161,7 @@ body.protofox-theme .navbar {
}
body.protofox-theme .navbar::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -167,7 +178,7 @@ body.protofox-theme .nav-link {
}
body.protofox-theme .nav-link::after {
content: '';
content: "";
position: absolute;
bottom: -2px;
left: 0;
@ -190,7 +201,7 @@ body.protofox-theme .webring-button {
}
body.protofox-theme .webring-button::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -213,7 +224,7 @@ body.protofox-theme .foxxos-container {
}
body.protofox-theme .foxxos-container::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;

View file

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

View file

@ -6,7 +6,7 @@
}
.animated-bg::before {
content: '';
content: "";
position: absolute;
inset: 0;
opacity: 0.5;
@ -69,7 +69,8 @@
}
@keyframes gradientShift {
0%, 100% {
0%,
100% {
background-position: 0% 50%;
}
50% {

View file

@ -5,21 +5,21 @@ export interface Position {
export interface PowerUp {
id: string;
type: 'SPEED' | 'SHIELD' | 'MAGNET';
type: "SPEED" | "SHIELD" | "MAGNET";
duration: number;
position: Position;
}
export interface Collectible {
id: string;
type: 'STAR' | 'GEM' | 'KEY';
type: "STAR" | "GEM" | "KEY";
value: number;
position: Position;
}
export interface Enemy {
id: string;
type: 'WOLF' | 'OWL' | 'HUNTER';
type: "WOLF" | "OWL" | "HUNTER";
position: Position;
direction: Position;
speed: number;
@ -41,7 +41,7 @@ export interface GameState {
powerUps: PowerUp[];
score: number;
level: number;
gameStatus: 'MENU' | 'PLAYING' | 'PAUSED' | 'GAME_OVER';
gameStatus: "MENU" | "PLAYING" | "PAUSED" | "GAME_OVER";
highScores: number[];
timePlayed: number;

View file

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

View file

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