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 ( - -
-
-
- Background Logo -
-
- -
- -
- - } /> - } /> - } /> - -

404: Page Not Found

-

This fox couldn't find what you're looking for.

+ + + +
+ {/* Background Logo */} +
+
+ Background Logo +
+
+ + {/* Main Content */} +
+ +
+ + } /> + } /> + } /> + } /> + } /> + + + + } + /> + +

404: Page Not Found

+

This fox couldn't find what you're looking for.

+
+ } /> + +
+ + {/* Footer */} +
+

© 2023 - {new Date().getFullYear()} EndofTimee. All rights reserved.

+
+ Try the Konami code: ↑↑↓↓←→←→BA +
+ v1.3.0 +
- } /> - - -
+ +
- {isGameActive && } - -
+ {/* Demo Switch Notification */} + setShowInitialSwitchDemo(false)} + alterName="Aurora" + type="switch" + autoClose + autoCloseDelay={5000} + /> + + {/* Fox Game Overlay - Activated by Konami Code */} + {isGameActive && ( + <> + + + + )} + + + + ); }; -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 ( +
+
+
+
+ System Status +
+ +
+
+ ); + } + + // Full or expanded minimal view + return ( + +
+
+ +

System Status

+
+ {minimal && ( + + )} +
+ + {lastUpdate ? `Updated: ${lastUpdate.toLocaleTimeString()}` : 'Not updated'} + +
+
+
+ +
+ {/* Safety Level Selector */} +
+
+ + Safety Level: +
+
+ {safetyLevels.map(level => ( + + ))} +
+
+ + {/* Mental State Selector */} +
+
+ + Mental State: +
+
+ {mentalStates.map(state => ( + + ))} +
+
+ + {/* Fronting Status Selector */} +
+
+ + Fronting Status: +
+
+ {frontingStatuses.map(status => ( + + ))} +
+
+ + {/* Current Fronters */} +
+
+ + Current Fronters: +
+
+ {systemMembers.slice(0, 3).map(member => ( +
+ {member.name} ({member.role}) +
+ ))} + +
+
+
+ + {!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 && ( + <> + + + + )} -
@@ -49,25 +126,59 @@ const VNCViewer = () => { {isConnected ? (