diff --git a/.env.example b/.env.example
index fea7fa8..9b3d42b 100644
--- a/.env.example
+++ b/.env.example
@@ -2,8 +2,7 @@
# Get your API key at: https://www.last.fm/api/account/create
VITE_LASTFM_API_KEY=your_api_key_here
VITE_LASTFM_USERNAME=your_lastfm_username
-
-# CloudFlare Configuration
+VITE_SIMPLYLURAL_API_KEY=YourSPKey
# Get these from your Cloudflare dashboard: https://dash.cloudflare.com/
CLOUDFLARE_API_TOKEN=your_cloudflare_api_token
CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id
\ No newline at end of file
diff --git a/deploy-master.ps1 b/deploy-master.ps1
deleted file mode 100644
index c8c06b2..0000000
--- a/deploy-master.ps1
+++ /dev/null
@@ -1,119 +0,0 @@
-# deploy.ps1
-Set-StrictMode -Version Latest
-$ErrorActionPreference = 'Stop'
-
-function Write-Status {
- param(
- [string]$Message,
- [string]$Type = "Info"
- )
-
- $colors = @{
- Info = "Cyan"
- Success = "Green"
- Warning = "Yellow"
- Error = "Red"
- }
-
- $prefix = switch ($Type) {
- "Success" { "[+]" }
- "Error" { "[-]" }
- "Warning" { "[!]" }
- "Info" { "[*]" }
- }
-
- Write-Host "$prefix $Message" -ForegroundColor $colors[$Type]
-}
-
-function Test-Environment {
- $requiredVars = @{
- # "CLOUDFLARE_API_TOKEN" = "For Cloudflare API access"
- "CLOUDFLARE_ACCOUNT_ID" = "For Cloudflare account identification"
- }
-
- $missingVars = @()
- foreach ($var in $requiredVars.GetEnumerator()) {
- if (-not (Get-Item env:$($var.Key) -ErrorAction SilentlyContinue)) {
- $message = "$($var.Key) is missing"
- if ($var.Value) {
- $message += ": $($var.Value)"
- }
- Write-Status $message "Warning"
- $missingVars += $var.Key
- }
- }
-
- if ($missingVars.Count -gt 0) {
- throw "Missing required environment variables: $($missingVars -join ', ')"
- }
-}
-
-function Clear-BuildArtifacts {
- $paths = @("dist", "node_modules/.cache")
- foreach ($path in $paths) {
- if (Test-Path $path) {
- Remove-Item -Recurse -Force $path
- Write-Status "Cleaned $path" "Success"
- }
- }
-}
-
-function Install-Dependencies {
- Write-Status "Installing dependencies..." "Info"
- npm ci --prefer-offline --no-audit
- if ($LASTEXITCODE -ne 0) {
- npm install
- if ($LASTEXITCODE -ne 0) { throw "Failed to install dependencies" }
- }
- Write-Status "Dependencies installed successfully" "Success"
-}
-
-function Start-Build {
- Write-Status "Building project..." "Info"
- npm run build
- if ($LASTEXITCODE -ne 0) { throw "Frontend build failed" }
-
- if (-not (Test-Path "dist")) { throw "Build failed - dist directory not created" }
- Write-Status "Build completed successfully" "Success"
-}
-
-function Deploy-Frontend {
- Write-Status "Deploying to Cloudflare Pages..." "Info"
-
- npx wrangler pages deploy dist/
- if ($LASTEXITCODE -ne 0) { throw "Pages deployment failed" }
-
- Write-Status "Pages deployed successfully" "Success"
-}
-
-function Start-Deployment {
- try {
- # Create log directory if it doesn't exist
- if (-not (Test-Path "logs")) { New-Item -ItemType Directory -Path "logs" }
-
- # Start logging
- $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
- $script:logFile = "logs/deploy_$timestamp.log"
- Start-Transcript -Path $script:logFile
-
- Test-Environment
- Clear-BuildArtifacts
- Install-Dependencies
- Start-Build
- Deploy-Frontend
-
- Write-Status "Deployment completed successfully!" "Success"
- Write-Status "Log file: $script:logFile" "Info"
- }
- catch {
- Write-Status "Deployment failed: $_" "Error"
- Write-Status "Check the log file for details: $script:logFile" "Info"
- exit 1
- }
- finally {
- Stop-Transcript
- }
-}
-
-# Start deployment
-Start-Deployment
\ No newline at end of file
diff --git a/first-deploy.ps1 b/first-deploy.ps1
deleted file mode 100644
index 6f74cfb..0000000
--- a/first-deploy.ps1
+++ /dev/null
@@ -1,188 +0,0 @@
-# first-time-setup.ps1
-Set-StrictMode -Version Latest
-$ErrorActionPreference = 'Stop'
-
-function Write-Status {
- param(
- [string]$Message,
- [string]$Type = "Info"
- )
-
- $colors = @{
- Info = "Cyan"
- Success = "Green"
- Warning = "Yellow"
- Error = "Red"
- Input = "Magenta"
- }
-
- $prefix = switch ($Type) {
- "Success" { "[+]" }
- "Error" { "[-]" }
- "Warning" { "[!]" }
- "Info" { "[*]" }
- "Input" { "[?]" }
- }
-
- Write-Host "$prefix $Message" -ForegroundColor $colors[$Type]
-}
-
-function Get-UserInput {
- param(
- [string]$Prompt,
- [switch]$IsPassword
- )
-
- Write-Status $Prompt "Input"
- if ($IsPassword) {
- $secureString = Read-Host -AsSecureString
- $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
- $string = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
- return $string
- } else {
- return Read-Host
- }
-}
-
-function Open-BrowserIfConfirmed {
- param([string]$Url, [string]$Message)
-
- Write-Status $Message "Info"
- $response = Get-UserInput "Would you like to open the browser now? (y/n)"
- if ($response -eq 'y') {
- Start-Process $Url
- Write-Status "Browser opened. Press Enter once you've completed this step." "Input"
- Read-Host | Out-Null
- }
-}
-
-function Install-RequiredTools {
- Write-Status "Checking required tools..." "Info"
-
- # Check Node.js
- if (-not (Get-Command "node" -ErrorAction SilentlyContinue)) {
- Write-Status "Node.js is not installed. Please install it from: https://nodejs.org/" "Error"
- Open-BrowserIfConfirmed "https://nodejs.org/" "Opening Node.js download page..."
- throw "Node.js is required to continue"
- }
-
- # Check Git
- if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) {
- Write-Status "Git is not installed. Please install it from: https://git-scm.com/" "Error"
- Open-BrowserIfConfirmed "https://git-scm.com/downloads" "Opening Git download page..."
- throw "Git is required to continue"
- }
-
- # Install wrangler globally
- Write-Status "Installing Wrangler CLI..." "Info"
- npm install -g wrangler
- if ($LASTEXITCODE -ne 0) { throw "Failed to install Wrangler" }
-}
-
-function Setup-CloudflareAccount {
- Write-Status "Setting up Cloudflare account..." "Info"
- Write-Status "If you don't have a Cloudflare account, you'll need to create one." "Info"
- Open-BrowserIfConfirmed "https://dash.cloudflare.com/sign-up" "Opening Cloudflare signup page..."
-
- # Login to Cloudflare using Wrangler
- Write-Status "Logging in to Cloudflare..." "Info"
- npx wrangler login
- if ($LASTEXITCODE -ne 0) { throw "Failed to login to Cloudflare" }
-
- # Get Account ID
- Write-Status "Please get your Cloudflare Account ID from the Cloudflare Dashboard." "Info"
- Write-Status "You can find it at https://dash.cloudflare.com/ in the right sidebar." "Info"
- Open-BrowserIfConfirmed "https://dash.cloudflare.com/" "Opening Cloudflare Dashboard..."
-
- $accountId = Get-UserInput "Enter your Cloudflare Account ID"
- $apiToken = Get-UserInput "Enter your Cloudflare API Token (from https://dash.cloudflare.com/profile/api-tokens)" -IsPassword
-
- # Create .env.local file
- Write-Status "Creating .env.local file..." "Info"
- $envContent = @"
-CLOUDFLARE_API_TOKEN=$apiToken
-CLOUDFLARE_ACCOUNT_ID=$accountId
-VITE_LASTFM_API_KEY=
-VITE_LASTFM_USERNAME=
-"@
- Set-Content -Path ".env.local" -Value $envContent
- Write-Status "Created .env.local file with Cloudflare credentials" "Success"
-}
-
-function Setup-Project {
- Write-Status "Setting up project..." "Info"
-
- # Install dependencies
- Write-Status "Installing project dependencies..." "Info"
- npm install
- if ($LASTEXITCODE -ne 0) { throw "Failed to install dependencies" }
-
- # Initialize git if not already initialized
- if (-not (Test-Path ".git")) {
- Write-Status "Initializing git repository..." "Info"
- git init
- git add .
- git commit -m "Initial commit"
- }
-
- # Create initial wrangler.toml if it doesn't exist
- if (-not (Test-Path "wrangler.toml")) {
- Write-Status "Creating wrangler.toml..." "Info"
- $wranglerContent = @"
-name = "personal-site"
-main = "src/worker/index.ts"
-compatibility_date = "2024-02-01"
-
-[build]
-command = "npm run build:worker"
-
-[env.production]
-name = "personal-site"
-vars = { ENVIRONMENT = "production" }
-
-[env.development]
-name = "personal-site-dev"
-vars = { ENVIRONMENT = "development" }
-
-[dev]
-port = 8787
-"@
- Set-Content -Path "wrangler.toml" -Value $wranglerContent
- }
-}
-
-function Start-FirstTimeSetup {
- try {
- Write-Status "Starting first-time setup..." "Info"
-
- # Create log directory if it doesn't exist
- if (-not (Test-Path "logs")) {
- New-Item -ItemType Directory -Path "logs" | Out-Null
- }
-
- # Start logging
- $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
- $logFile = "logs/setup_$timestamp.log"
- Start-Transcript -Path $logFile
-
- # Run setup steps
- Install-RequiredTools
- Setup-CloudflareAccount
- Setup-Project
-
- Write-Status "First-time setup completed successfully!" "Success"
- Write-Status "You can now run './deploy-master.ps1' to deploy your site" "Info"
- Write-Status "Log file: $logFile" "Info"
- }
- catch {
- Write-Status "Setup failed: $_" "Error"
- Write-Status "Check the log file for details: $logFile" "Info"
- exit 1
- }
- finally {
- Stop-Transcript
- }
-}
-
-# Start setup
-Start-FirstTimeSetup
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index 3ab3ab0..a25f602 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,16 +1,123 @@
-// src/App.tsx
+// src/App.tsx
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
+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 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 { useState, useEffect } from "react";
+import '@/styles/animations.css';
+
+// AuthChecker component to access auth context inside the router
+const AuthChecker = ({ children }: { children: React.ReactNode }) => {
+ const auth = useAuth();
+ const [isStatusVisible, setIsStatusVisible] = useState(false);
+ const [showNotification, setShowNotification] = useState(false);
+ const [notificationType, setNotificationType] = useState<'switch' | 'warning' | 'notice'>('switch');
+ const [notificationMessage, setNotificationMessage] = useState('');
+ const [selectedAlter, setSelectedAlter] = useState('');
+
+ // Toggle system status floating panel
+ const toggleStatus = () => {
+ setIsStatusVisible(prev => !prev);
+ };
+
+ // Simulate random switches for demo purposes
+ useEffect(() => {
+ if (!auth.isAuthenticated) return;
+
+ // Every 5-15 minutes, show a switch notification
+ const randomInterval = Math.floor(Math.random() * (15 - 5 + 1) + 5) * 60 * 1000;
+ const interval = setInterval(() => {
+ // 70% chance of switch, 20% chance of notice, 10% chance of warning
+ const rand = Math.random();
+
+ if (rand < 0.7) {
+ setNotificationType('switch');
+ setSelectedAlter(''); // Random alter will be selected
+ } else if (rand < 0.9) {
+ setNotificationType('notice');
+ setNotificationMessage('System communication active');
+ } else {
+ setNotificationType('warning');
+ setNotificationMessage('System experiencing stress');
+ }
+
+ setShowNotification(true);
+ }, randomInterval);
+
+ return () => clearInterval(interval);
+ }, [auth.isAuthenticated]);
+
+ return (
+ <>
+ {children}
+
+ {/* Floating System Status for authenticated users */}
+ {auth.isAuthenticated && (
+ <>
+
+
+
+ {isStatusVisible ? "Hide System Status" : "System Status"}
+
+
+
+
+
+ {/* System Notifications */}
+ setShowNotification(false)}
+ alterName={selectedAlter}
+ type={notificationType}
+ message={notificationMessage}
+ autoClose
+ autoCloseDelay={5000}
+ />
+ >
+ )}
+ >
+ );
+};
const App = () => {
const [isGameActive, setIsGameActive] = useState(false);
+ const [showInitialSwitchDemo, setShowInitialSwitchDemo] = useState(false);
+
+ // Demo the switch notification after a delay
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setShowInitialSwitchDemo(true);
+
+ // Hide after 5 seconds
+ setTimeout(() => {
+ setShowInitialSwitchDemo(false);
+ }, 5000);
+ }, 10000);
+
+ return () => clearTimeout(timer);
+ }, []);
useEffect(() => {
+ // Konami code sequence
const konamiCode = [
'ArrowUp', 'ArrowUp',
'ArrowDown', 'ArrowDown',
@@ -21,54 +128,110 @@ const App = () => {
let index = 0;
const handleKeydown = (event: KeyboardEvent) => {
+ // Check if the pressed key matches the next key in the sequence
if (event.key === konamiCode[index]) {
index++;
+
+ // If the entire sequence is completed
if (index === konamiCode.length) {
setIsGameActive(true);
+ // Optional: Play a sound or show a notification
+ console.log('Konami code activated!');
}
} else {
+ // Reset if a wrong key is pressed
index = 0;
}
};
window.addEventListener('keydown', handleKeydown);
+
+ // Clean up the event listener on component unmount
return () => window.removeEventListener('keydown', handleKeydown);
}, []);
return (
-
-
-
-
-
-
-
-
-
-
-
-
- } />
- } />
- } />
-
- 404: Page Not Found
- This fox couldn't find what you're looking for.
+
+
+
+
+ {/* Background Logo */}
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+
+ 404: Page Not Found
+ This fox couldn't find what you're looking for.
+
+ } />
+
+
+
+ {/* Footer */}
+
+
+
- {isGameActive &&
}
-
-
+ {/* Demo Switch Notification */}
+ setShowInitialSwitchDemo(false)}
+ alterName="Aurora"
+ type="switch"
+ autoClose
+ autoCloseDelay={5000}
+ />
+
+ {/* Fox Game Overlay - Activated by Konami Code */}
+ {isGameActive && (
+ <>
+
+ setIsGameActive(false)}
+ className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors"
+ >
+ Exit Game
+
+ >
+ )}
+
+
+
+
);
};
-export default App;
\ No newline at end of file
+export default App;
diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..ef737f7
--- /dev/null
+++ b/src/components/ProtectedRoute.tsx
@@ -0,0 +1,27 @@
+import { Navigate, useLocation } from 'react-router-dom';
+import { useAuth } from '@/context/AuthContext';
+import { ReactNode } from 'react';
+
+interface ProtectedRouteProps {
+ children: ReactNode;
+}
+
+/**
+ * A wrapper component that protects routes requiring authentication
+ * Redirects to login if user is not authenticated, preserving the intended destination
+ */
+const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
+ const auth = useAuth();
+ const location = useLocation();
+
+ if (!auth.isAuthenticated) {
+ // Redirect to login page while saving the attempted location
+ // This enables seamless redirection back after successful login
+ return ;
+ }
+
+ // If authenticated, render the protected content
+ return <>{children}>;
+};
+
+export default ProtectedRoute;
diff --git a/src/components/SwitchNotification.tsx b/src/components/SwitchNotification.tsx
new file mode 100644
index 0000000..225f73e
--- /dev/null
+++ b/src/components/SwitchNotification.tsx
@@ -0,0 +1,172 @@
+import { useEffect, useState } from 'react';
+import { RefreshCw, AlertCircle, X } from 'lucide-react';
+import { systemMembers } from '@/context/AuthContext';
+
+interface SwitchNotificationProps {
+ show: boolean;
+ onClose: () => void;
+ alterName?: string;
+ type?: 'switch' | 'warning' | 'notice';
+ message?: string;
+ autoClose?: boolean;
+ autoCloseDelay?: number;
+}
+
+const SwitchNotification = ({
+ show,
+ onClose,
+ alterName,
+ type = 'switch',
+ message,
+ autoClose = true,
+ autoCloseDelay = 5000
+}: SwitchNotificationProps) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [progress, setProgress] = useState(100);
+ const [progressInterval, setProgressInterval] = useState(null);
+
+ // Random alter selection if none provided
+ const [selectedAlter, setSelectedAlter] = useState(alterName);
+
+ useEffect(() => {
+ if (!alterName && show) {
+ // Select a random alter if none provided
+ const randomIndex = Math.floor(Math.random() * systemMembers.length);
+ setSelectedAlter(systemMembers[randomIndex].name);
+ } else if (alterName) {
+ setSelectedAlter(alterName);
+ }
+ }, [alterName, show]);
+
+ // Handle visibility state
+ useEffect(() => {
+ if (show) {
+ setIsVisible(true);
+ setProgress(100);
+ } else {
+ setIsVisible(false);
+ }
+ }, [show]);
+
+ // Auto-close countdown
+ useEffect(() => {
+ if (autoClose && isVisible) {
+ if (progressInterval) {
+ clearInterval(progressInterval);
+ }
+
+ const interval = setInterval(() => {
+ setProgress(prev => {
+ const newProgress = prev - (100 / (autoCloseDelay / 100));
+ if (newProgress <= 0) {
+ clearInterval(interval);
+ handleClose();
+ return 0;
+ }
+ return newProgress;
+ });
+ }, 100);
+
+ setProgressInterval(interval);
+
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }
+ }, [autoClose, isVisible, autoCloseDelay]);
+
+ // Handle close action
+ const handleClose = () => {
+ setIsVisible(false);
+ if (progressInterval) {
+ clearInterval(progressInterval);
+ setProgressInterval(null);
+ }
+ setTimeout(() => {
+ onClose();
+ }, 300);
+ };
+
+ // Don't render if not visible
+ if (!show && !isVisible) return null;
+
+ // Get the right icon and styles based on notification type
+ const getIconAndStyles = () => {
+ switch (type) {
+ case 'warning':
+ return {
+ icon: ,
+ bgColor: 'bg-yellow-500/20',
+ borderColor: 'border-yellow-500/30',
+ textColor: 'text-yellow-300'
+ };
+ case 'notice':
+ return {
+ icon: ,
+ bgColor: 'bg-blue-500/20',
+ borderColor: 'border-blue-500/30',
+ textColor: 'text-blue-300'
+ };
+ case 'switch':
+ default:
+ return {
+ icon: ,
+ bgColor: 'bg-accent-primary/20',
+ borderColor: 'border-accent-primary/30',
+ textColor: 'text-accent-primary'
+ };
+ }
+ };
+
+ const { icon, bgColor, borderColor, textColor } = getIconAndStyles();
+
+ return (
+
+
+ {/* Title row with icon, text and close button */}
+
+
+ {icon}
+
+ {type === 'switch' ? 'System Switch Detected' : type === 'warning' ? 'System Warning' : 'System Notice'}
+
+
+
+
+
+
+
+ {/* Message content */}
+
+ {type === 'switch' ? (
+ <>Now fronting: {selectedAlter} >
+ ) : (
+ message || 'System notification'
+ )}
+
+
+ {/* Progress bar for auto-close */}
+ {autoClose && (
+
+ )}
+
+
+ );
+};
+
+export default SwitchNotification;
diff --git a/src/components/SystemStatus.tsx b/src/components/SystemStatus.tsx
new file mode 100644
index 0000000..a0b7a2b
--- /dev/null
+++ b/src/components/SystemStatus.tsx
@@ -0,0 +1,271 @@
+import { useState, useEffect } from 'react';
+import { useAuth, systemMembers } from '@/context/AuthContext';
+import FoxCard from '@/components/FoxCard';
+import {
+ Shield,
+ AlertCircle,
+ Brain,
+ Users,
+ ChevronDown,
+ ChevronUp,
+ RefreshCw
+} from 'lucide-react';
+
+interface SystemStatusProps {
+ minimal?: boolean;
+ className?: string;
+}
+
+const SystemStatus = ({ minimal = false, className = '' }: SystemStatusProps) => {
+ const { systemState, updateSystemState } = useAuth();
+ const [expanded, setExpanded] = useState(!minimal);
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ // Safety level options with corresponding colors and icons
+ const safetyLevels = [
+ { value: 'safe', label: 'Safe', color: 'bg-green-500', textColor: 'text-green-500' },
+ { value: 'sorta-safe', label: 'Somewhat Safe', color: 'bg-yellow-500', textColor: 'text-yellow-500' },
+ { value: 'unsafe', label: 'Unsafe', color: 'bg-red-500', textColor: 'text-red-500' },
+ { value: 'unknown', label: 'Unknown', color: 'bg-gray-500', textColor: 'text-gray-500' }
+ ];
+
+ // Mental state options
+ const mentalStates = [
+ { value: 'ok', label: 'OK', color: 'bg-green-500', textColor: 'text-green-500' },
+ { value: 'bad', label: 'Bad', color: 'bg-yellow-500', textColor: 'text-yellow-500' },
+ { value: 'very-bad', label: 'Very Bad', color: 'bg-red-500', textColor: 'text-red-500' },
+ { value: 'panic', label: 'Panic', color: 'bg-red-500', textColor: 'text-red-500' },
+ { value: 'spiraling', label: 'Spiraling', color: 'bg-purple-500', textColor: 'text-purple-500' },
+ { value: 'unstable', label: 'Unstable', color: 'bg-orange-500', textColor: 'text-orange-500' },
+ { value: 'delusional', label: 'Delusional', color: 'bg-pink-500', textColor: 'text-pink-500' }
+ ];
+
+ // Fronting status options
+ const frontingStatuses = [
+ { value: 'single', label: 'Single Fronting', color: 'bg-blue-500', textColor: 'text-blue-500' },
+ { value: 'co-fronting', label: 'Co-Fronting', color: 'bg-purple-500', textColor: 'text-purple-500' },
+ { value: 'switching', label: 'Switching', color: 'bg-orange-500', textColor: 'text-orange-500' },
+ { value: 'unknown', label: 'Unknown', color: 'bg-gray-500', textColor: 'text-gray-500' }
+ ];
+
+ // Check if the status has been updated in the last 5 minutes
+ const [isFresh, setIsFresh] = useState(true);
+ const [lastUpdate, setLastUpdate] = useState(null);
+
+ useEffect(() => {
+ // Update the "freshness" status every minute
+ const updateFreshness = () => {
+ if (!lastUpdate) return;
+
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
+ setIsFresh(lastUpdate > fiveMinutesAgo);
+ };
+
+ setLastUpdate(new Date());
+ const interval = setInterval(updateFreshness, 60000);
+
+ return () => clearInterval(interval);
+ }, [systemState?.safetyLevel, systemState?.mentalState, systemState?.frontingStatus]);
+
+ // Get colored representation of current states
+ const getCurrentSafetyLevel = () => {
+ if (!systemState) return safetyLevels[3]; // Unknown
+ return safetyLevels.find(level => level.value === systemState.safetyLevel) || safetyLevels[3];
+ };
+
+ const getCurrentMentalState = () => {
+ if (!systemState) return mentalStates[6]; // Unknown
+ return mentalStates.find(state => state.value === systemState.mentalState) || mentalStates[6];
+ };
+
+ const getCurrentFrontingStatus = () => {
+ if (!systemState) return frontingStatuses[3]; // Unknown
+ return frontingStatuses.find(status => status.value === systemState.frontingStatus) || frontingStatuses[3];
+ };
+
+ // Handle status change
+ const updateStatus = (type: string, value: string) => {
+ setIsUpdating(true);
+
+ // This would typically involve an API call in a production environment
+ setTimeout(() => {
+ if (type === 'safety') {
+ updateSystemState({ safetyLevel: value as any });
+ } else if (type === 'mental') {
+ updateSystemState({ mentalState: value as any });
+ } else if (type === 'fronting') {
+ updateSystemState({ frontingStatus: value as any });
+ }
+
+ setLastUpdate(new Date());
+ setIsFresh(true);
+ setIsUpdating(false);
+ }, 300);
+ };
+
+ // Toggle expansion for mobile/minimal view
+ const toggleExpand = () => {
+ setExpanded(!expanded);
+ };
+
+ if (!systemState) return null;
+
+ // For minimal mode, show just a summary with expand option
+ if (minimal && !expanded) {
+ return (
+
+ );
+ }
+
+ // Full or expanded minimal view
+ return (
+
+
+
+
+
System Status
+
+ {minimal && (
+
+
+
+ )}
+
+
+ {lastUpdate ? `Updated: ${lastUpdate.toLocaleTimeString()}` : 'Not updated'}
+
+
+
+
+
+
+ {/* Safety Level Selector */}
+
+
+
+ Safety Level:
+
+
+ {safetyLevels.map(level => (
+ updateStatus('safety', level.value)}
+ disabled={isUpdating}
+ className={`px-3 py-1 rounded-full text-xs border transition-colors ${
+ systemState.safetyLevel === level.value
+ ? `bg-background-secondary border-${level.color} ${level.textColor}`
+ : 'bg-background-primary/30 border-transparent hover:border-gray-600'
+ }`}
+ >
+ {level.label}
+
+ ))}
+
+
+
+ {/* Mental State Selector */}
+
+
+
+ Mental State:
+
+
+ {mentalStates.map(state => (
+ updateStatus('mental', state.value)}
+ disabled={isUpdating}
+ className={`px-3 py-1 rounded-full text-xs border transition-colors ${
+ systemState.mentalState === state.value
+ ? `bg-background-secondary border-${state.color} ${state.textColor}`
+ : 'bg-background-primary/30 border-transparent hover:border-gray-600'
+ }`}
+ >
+ {state.label}
+
+ ))}
+
+
+
+ {/* Fronting Status Selector */}
+
+
+
+ Fronting Status:
+
+
+ {frontingStatuses.map(status => (
+ updateStatus('fronting', status.value)}
+ disabled={isUpdating}
+ className={`px-3 py-1 rounded-full text-xs border transition-colors ${
+ systemState.frontingStatus === status.value
+ ? `bg-background-secondary border-${status.color} ${status.textColor}`
+ : 'bg-background-primary/30 border-transparent hover:border-gray-600'
+ }`}
+ >
+ {status.label}
+
+ ))}
+
+
+
+ {/* Current Fronters */}
+
+
+
+ {systemMembers.slice(0, 3).map(member => (
+
+ {member.name} ({member.role})
+
+ ))}
+
+
+ Change
+
+
+
+
+
+ {!isFresh && (
+
+ Status was last updated over 5 minutes ago. It may not reflect current conditions.
+
+ )}
+
+ {isUpdating && (
+
+ )}
+
+ );
+};
+
+export default SystemStatus;
diff --git a/src/components/VNCViewer.tsx b/src/components/VNCViewer.tsx
index a108d60..2774ba7 100644
--- a/src/components/VNCViewer.tsx
+++ b/src/components/VNCViewer.tsx
@@ -1,23 +1,80 @@
-import { useState } from 'react';
-import { Monitor, Power, Lock } from 'lucide-react';
+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(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();
+ iframe.requestFullscreen().catch(err => {
+ setError(`Fullscreen error: ${err.message}`);
+ });
} else {
- document.exitFullscreen();
+ 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 (
-
-
+
+
+ Remote Access
+ Raspberry Pi VNC Connection
+
+
+
@@ -25,21 +82,41 @@ const VNCViewer = () => {
+ {isConnected && (
+ <>
+
+
+
+
+ {isFullscreen ? (
+
+ ) : (
+
+ )}
+
+ >
+ )}
-
-
-
setIsConnected(!isConnected)}
- className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
- title="Toggle Connection"
+ 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}
>
@@ -49,25 +126,59 @@ const VNCViewer = () => {
{isConnected ? (
+ ) : isLoading ? (
+
+
+
Connecting to VNC server...
+
) : (
Click the power button to connect
)}
+
+ {error && (
+
+ {error}
+
+ )}
-
-
Status: {isConnected ? 'Connected' : 'Disconnected'}
-
Press ESC to exit fullscreen
+
+
+
+
Status: {isConnected ? 'Connected' : 'Disconnected'}
+
+
+
+ Server: {VNC_HOST}
+ Press ESC to exit fullscreen
+
+
+
+
+
+ Connection Instructions
+
+ Click the power button in the top right to connect
+ Once connected, you can interact with the remote desktop
+ Use the maximize button to enter fullscreen mode
+ If the connection is unresponsive, try clicking the refresh button
+
+
+
+
Note: This VNC connection is not encrypted. Do not transmit sensitive information when using this interface.
+
+
);
};
-export default VNCViewer;
\ No newline at end of file
+export default VNCViewer;
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 0000000..9d55b7d
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,164 @@
+import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
+
+interface SystemMember {
+ id: string;
+ name: string;
+ role: string;
+ color?: string;
+}
+
+interface SystemState {
+ safetyLevel: 'safe' | 'unsafe' | 'sorta-safe' | 'unknown';
+ mentalState: 'ok' | 'bad' | 'very-bad' | 'panic' | 'spiraling' | 'unstable' | 'delusional';
+ frontingStatus: 'single' | 'co-fronting' | 'switching' | 'unknown';
+ currentFronters: SystemMember[];
+}
+
+interface AuthContextType {
+ isAuthenticated: boolean;
+ username: string | null;
+ systemState: SystemState | null;
+ login: (username: string, password: string) => Promise;
+ logout: () => void;
+ updateSystemState: (newState: Partial) => void;
+}
+
+// System members data
+const systemMembers: SystemMember[] = [
+ { id: '1', name: 'Aurora', role: 'Host', color: '#9d4edd' },
+ { id: '2', name: 'Alex', role: 'Younger', color: '#4ea8de' },
+ { id: '3', name: 'Psy', role: 'Protector', color: '#5e548e' },
+ { id: '4', name: 'Xander', role: 'Caretaker', color: '#219ebc' },
+ { id: '5', name: 'Unknown', role: 'Fragment', color: '#6c757d' },
+ { id: '6', name: 'The thing', role: 'Persecutor', color: '#e63946' },
+ { id: '7', name: 'Unknown 2', role: 'Fragment', color: '#6c757d' },
+];
+
+// Creating the context with a default value of null
+const AuthContext = createContext(null);
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ // Initialize authentication state from localStorage if available
+ const [isAuthenticated, setIsAuthenticated] = useState(() => {
+ const stored = localStorage.getItem('isAuthenticated');
+ return stored === 'true';
+ });
+
+ const [username, setUsername] = useState(() => {
+ return localStorage.getItem('username');
+ });
+
+ // Initialize system state from localStorage or set defaults
+ const [systemState, setSystemState] = useState(() => {
+ const stored = localStorage.getItem('systemState');
+ if (stored && isAuthenticated) {
+ return JSON.parse(stored);
+ }
+ return isAuthenticated ? {
+ safetyLevel: 'safe',
+ mentalState: 'ok',
+ frontingStatus: 'single',
+ currentFronters: [systemMembers[0]] // Default to Aurora as fronter
+ } : null;
+ });
+
+ // Update localStorage when auth state changes
+ useEffect(() => {
+ localStorage.setItem('isAuthenticated', isAuthenticated.toString());
+ if (username) {
+ localStorage.setItem('username', username);
+ } else {
+ localStorage.removeItem('username');
+ }
+
+ // If logged out, clear system state
+ if (!isAuthenticated) {
+ localStorage.removeItem('systemState');
+ setSystemState(null);
+ } else if (systemState) {
+ localStorage.setItem('systemState', JSON.stringify(systemState));
+ }
+ }, [isAuthenticated, username, systemState]);
+
+ const login = async (username: string, password: string) => {
+ // For security, add a slight delay to prevent rapid brute force attempts
+ await new Promise(resolve => setTimeout(resolve, 800));
+
+ // We use credential verification with multiple allowed passwords for different contexts
+ const validCredentials = [
+ { user: 'system', pass: '.' },
+ ];
+
+ const isValid = validCredentials.some(
+ cred => cred.user === username.toLowerCase() && cred.pass === password
+ );
+
+ if (isValid) {
+ setIsAuthenticated(true);
+ setUsername(username);
+
+ // Initialize system state on login
+ const initialState: SystemState = {
+ safetyLevel: 'safe',
+ mentalState: 'ok',
+ frontingStatus: 'single',
+ currentFronters: [systemMembers[0]]
+ };
+
+ setSystemState(initialState);
+ localStorage.setItem('systemState', JSON.stringify(initialState));
+
+ return true;
+ }
+
+ return false;
+ };
+
+ const logout = () => {
+ // Add a short delay for better UX
+ setTimeout(() => {
+ setIsAuthenticated(false);
+ setUsername(null);
+ setSystemState(null);
+
+ // Clear sensitive data from localStorage
+ localStorage.removeItem('systemState');
+ }, 300);
+ };
+
+ const updateSystemState = (newState: Partial) => {
+ if (!systemState) return;
+
+ const updatedState = { ...systemState, ...newState };
+ setSystemState(updatedState);
+ localStorage.setItem('systemState', JSON.stringify(updatedState));
+ };
+
+ // Construct the context value
+ const contextValue: AuthContextType = {
+ isAuthenticated,
+ username,
+ systemState,
+ login,
+ logout,
+ updateSystemState
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+// Custom hook for easier context consumption
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
+
+// Export system members data for use in other components
+export { systemMembers };
diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx
index 46ca649..495102c 100644
--- a/src/pages/AboutPage.tsx
+++ b/src/pages/AboutPage.tsx
@@ -20,7 +20,7 @@ const AboutPage = () => {
About Me
- Transfem Foxgirl • {age} years old • Programmer & Streamer
+ End • ProtoFoxes • They/Them • {age} years old • Programmer & Streamer
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..702cc02
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,226 @@
+import { useState, useEffect } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useAuth } from '@/context/AuthContext';
+import FoxCard from '@/components/FoxCard';
+import { Lock, User, LogIn, AlertTriangle, Shield, Brain } from 'lucide-react';
+
+interface LocationState {
+ from?: {
+ pathname: string;
+ };
+}
+
+const LoginPage = () => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [loginAttempts, setLoginAttempts] = useState(0);
+ const [isLocked, setIsLocked] = useState(false);
+ const [lockCountdown, setLockCountdown] = useState(0);
+
+ const navigate = useNavigate();
+ const location = useLocation();
+ const auth = useAuth();
+
+ // Get the intended destination, or default to home
+ const locationState = location.state as LocationState;
+ const from = locationState?.from?.pathname || '/';
+
+ // Handle countdown timer for account lockout
+ useEffect(() => {
+ if (lockCountdown <= 0) return;
+
+ const timer = setTimeout(() => {
+ setLockCountdown(prevCount => prevCount - 1);
+ if (lockCountdown === 1) {
+ setIsLocked(false);
+ setLoginAttempts(0);
+ }
+ }, 1000);
+
+ return () => clearTimeout(timer);
+ }, [lockCountdown]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Check if account is locked
+ if (isLocked) return;
+
+ setError('');
+ setIsLoading(true);
+
+ try {
+ const success = await auth.login(username, password);
+
+ if (success) {
+ // Reset login attempts on successful login
+ setLoginAttempts(0);
+ navigate(from, { replace: true });
+ } else {
+ // Increment login attempts
+ const newAttempts = loginAttempts + 1;
+ setLoginAttempts(newAttempts);
+
+ // Lock account after 5 failed attempts
+ if (newAttempts >= 5) {
+ setIsLocked(true);
+ setLockCountdown(30); // 30 second lockout
+ setError('Too many failed attempts. Account locked for 30 seconds.');
+ } else {
+ setError(`Invalid username or password. ${5 - newAttempts} attempts remaining.`);
+ }
+ }
+ } catch (err) {
+ setError('An error occurred during login. Please try again later.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ System Access
+ Please authenticate to view system information
+
+
+
+
+ {/* Decorative fox ears in corners */}
+
+
+
+
+
+
+
System Authentication
+
+ {error && (
+
+ )}
+
+ {isLocked && (
+
+
+
+ Access Temporarily Locked
+
+
+ Please wait {lockCountdown} seconds before trying again.
+
+
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default LoginPage;
diff --git a/src/pages/SystemPage.tsx b/src/pages/SystemPage.tsx
new file mode 100644
index 0000000..a63989b
--- /dev/null
+++ b/src/pages/SystemPage.tsx
@@ -0,0 +1,235 @@
+import FoxCard from '@/components/FoxCard';
+import { Users, Heart, Brain, Shield, LogOut, AlertTriangle, Info, Calendar, Tag, Activity } from 'lucide-react';
+import { useAuth } from '@/context/AuthContext';
+import { useNavigate } from 'react-router-dom';
+import { useState } from 'react';
+
+const SystemPage = () => {
+ const auth = useAuth();
+ const navigate = useNavigate();
+ const [showFrontingInfo, setShowFrontingInfo] = useState(false);
+
+ const handleLogout = () => {
+ auth.logout();
+ navigate('/login');
+ };
+
+ return (
+
+
+ Our System
+
+ EndofTimee System • 7 Members • Est. May 15th, 2009
+
+
+ setShowFrontingInfo(!showFrontingInfo)}
+ className="px-4 py-2 bg-accent-primary/20 hover:bg-accent-primary/40 rounded-md transition-colors flex items-center gap-2"
+ >
+
+ {showFrontingInfo ? 'Hide Fronting Info' : 'Show Fronting Info'}
+
+
+
+ Log out
+
+
+
+
+
+
+
+
+
About Our System
+
+
+ We are a plural system, which means we're multiple distinct consciousnesses sharing one body.
+ Also known as Dissociative Identity Disorder (DID) or OSDD in clinical terms, plurality
+ is our lived experience of having separate identities with their own thoughts, feelings, and memories.
+
+
+
+ Our plurality is an integral part of who we are. The system developed as a response to early experiences,
+ with Aurora being the original system member who arrived on May 15th, 2009. We've grown as a system since then,
+ with each member playing an important role in our collective existence.
+
+
+ {showFrontingInfo && (
+
+
+
+
Current Fronting Status
+
+
+
+
Safety Level:
+
+
+ Safe
+
+
+
+
Mental State:
+
+
+ OK Enough
+
+
+
+
Interaction:
+
+
+ Ask before touching
+
+
+
+
Front Status:
+
+
+ Cofronting
+
+
+
+
+ )}
+
+
+
+
+
+
Our Members
+
+
+
+
+ Born May 15th, 2009 • 15 years old • She/her
+ Chaotic foxgirl who enjoys programming, talking with Alice, and cheese. Dislikes loud noise and math.
+
+
+
+
+ Arrived December 14th, 2023 • Around 10? • Alex/she/her
+ Younger alter who appears when Aurora feels like it. Refers to herself in the third person.
+
+
+
+
+ Arrived December 31st, 2024 • Unknown age • They/them
+ System protector who appears during high anxiety or stress. Very calm and logical. Helps stabilize the system.
+
+
+
+
+ Arrived December 16th, 2024 • Unknown age • They/them
+ Older brother vibes. Calm and collected. Helps maintain system stability.
+
+
+
+
+
+
+
Important Safety Notice
+
+
Our system contains fragments that may be in distress. If you notice concerning behavior, please provide support or help us reach out to trusted contacts.
+
+
+
+
+
+
+
Communication Guide
+
+
+ Our system is continually working on internal communication and cooperation. Different members may front (control the body) at different times, sometimes with co-fronting or switching between alters.
+
+
+
+
+
+
+ Interaction Guidelines
+
+
+
+
+ Always ask before physical contact
+
+
+
+ Address whoever is fronting by their name
+
+
+
+ Respect boundaries indicated by front status
+
+
+
+ Be patient during switches or blurry fronting
+
+
+
+
+
+
+
+ System Timeline
+
+
+
+
+
+
May 15th, 2009
+
System formation (Aurora's arrival)
+
+
+
+
+
+
November 19th, 2011
+
Traumatic experience, new fragment
+
+
+
+
+
+
December 2023 - Present
+
Period of system growth and discovery
+
+
+
+
+
+
+
+
+
+ A note on our privacy
+
+
+ We've chosen to share this aspect of ourselves with trusted individuals. We appreciate
+ respect for our privacy and understanding that our system's experience is unique.
+ Thank you for being an ally in our journey.
+
+
+
+
+
+ );
+};
+
+export default SystemPage;
diff --git a/src/styles/animations.css b/src/styles/animations.css
index 1160969..e5c688e 100644
--- a/src/styles/animations.css
+++ b/src/styles/animations.css
@@ -1,17 +1,188 @@
+/* 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; }
+}
+
@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); }
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
+
+@keyframes fadeIn {
+ 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); }
+}
+
+@keyframes slideInRight {
+ 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; }
+}
+
+@keyframes wiggle {
+ 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); }
+}
+
+@keyframes statusBlink {
+ 0%, 49% { opacity: 1; }
+ 50%, 100% { opacity: 0.7; }
+}
+
+/* Add animation classes */
.animate-float {
-animation: float 3s ease-in-out infinite;
+ animation: float 3s ease-in-out infinite;
}
+
+.animate-pulse {
+ animation: pulse 2s ease-in-out infinite;
+}
+
.animate-glow {
-animation: glow 2s ease-in-out infinite;
+ animation: glow 2s ease-in-out infinite;
}
-.content-spacing > * + * {
-margin-top: 2rem;
+
+.animate-spin {
+ animation: spin 1s linear infinite;
+}
+
+.animate-bounce {
+ animation: bounce 1s ease-in-out infinite;
+}
+
+.animate-fade-in {
+ animation: fadeIn 0.5s ease-in-out forwards;
+}
+
+.animate-fade-out {
+ animation: fadeOut 0.5s ease-in-out forwards;
+}
+
+.animate-slide-in-right {
+ animation: slideInRight 0.3s ease-out forwards;
+}
+
+.animate-slide-in-left {
+ animation: slideInLeft 0.3s ease-out forwards;
+}
+
+.animate-wiggle {
+ animation: wiggle 1s ease-in-out;
+}
+
+.animate-switch-glitch {
+ animation: switchGlitch 0.5s ease-in-out;
+}
+
+.animate-status-blink {
+ animation: statusBlink 1s infinite;
+}
+
+/* Delay Utilities */
+.delay-100 {
+ animation-delay: 100ms;
+}
+
+.delay-200 {
+ animation-delay: 200ms;
+}
+
+.delay-300 {
+ animation-delay: 300ms;
+}
+
+.delay-500 {
+ animation-delay: 500ms;
+}
+
+.delay-700 {
+ animation-delay: 700ms;
+}
+
+.delay-1000 {
+ animation-delay: 1000ms;
+}
+
+/* Duration Utilities */
+.duration-300 {
+ animation-duration: 300ms;
+}
+
+.duration-500 {
+ animation-duration: 500ms;
+}
+
+.duration-700 {
+ animation-duration: 700ms;
+}
+
+.duration-1000 {
+ animation-duration: 1000ms;
+}
+
+.duration-2000 {
+ animation-duration: 2000ms;
+}
+
+.duration-3000 {
+ animation-duration: 3000ms;
+}
+
+/* Animation behavior */
+.animation-once {
+ animation-iteration-count: 1;
+}
+
+.animation-twice {
+ animation-iteration-count: 2;
+}
+
+.animation-infinite {
+ animation-iteration-count: infinite;
+}
+
+/* Pause animations when prefers-reduced-motion */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
}