mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 18:35:13 +00:00
commit
2b58241c82
13 changed files with 1604 additions and 372 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
188
first-deploy.ps1
188
first-deploy.ps1
|
|
@ -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
|
||||
227
src/App.tsx
227
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 && (
|
||||
<>
|
||||
<div
|
||||
className={`fixed bottom-4 right-4 z-40 transition-transform duration-300 ${
|
||||
isStatusVisible ? 'translate-y-0' : 'translate-y-[calc(100%-40px)]'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="p-2 bg-background-secondary rounded-t-lg cursor-pointer flex justify-center items-center"
|
||||
onClick={toggleStatus}
|
||||
>
|
||||
<span className="text-xs font-medium">
|
||||
{isStatusVisible ? "Hide System Status" : "System Status"}
|
||||
</span>
|
||||
</div>
|
||||
<SystemStatus
|
||||
minimal={true}
|
||||
className="shadow-lg rounded-t-none w-[300px] max-w-[calc(100vw-2rem)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* System Notifications */}
|
||||
<SwitchNotification
|
||||
show={showNotification}
|
||||
onClose={() => 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 (
|
||||
<Router>
|
||||
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}>
|
||||
<div className="fixed inset-0 z-behind pointer-events-none">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src="/logo.jpg"
|
||||
alt="Background Logo"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Navbar />
|
||||
<main className="content-wrapper section-spacing">
|
||||
<Routes>
|
||||
<Route path="/" element={<AboutPage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/apcsp" element={<APCSPPage />} />
|
||||
<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>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<AuthChecker>
|
||||
<div className={`min-h-screen bg-background-primary ${isGameActive ? 'game-active' : ''}`}>
|
||||
{/* Background Logo */}
|
||||
<div className="fixed inset-0 z-behind pointer-events-none">
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
src="/logo.jpg"
|
||||
alt="Background Logo"
|
||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] opacity-[0.03] blur-[2px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="relative">
|
||||
<Navbar />
|
||||
<main className="content-wrapper section-spacing pb-20 animate-fade-in">
|
||||
<Routes>
|
||||
<Route path="/" element={<AboutPage />} />
|
||||
<Route path="/projects" element={<ProjectsPage />} />
|
||||
<Route path="/apcsp" element={<APCSPPage />} />
|
||||
<Route path="/vnc" element={<VNCViewer />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/system"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<SystemPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] space-y-4">
|
||||
<h1 className="text-4xl font-bold text-glow">404: Page Not Found</h1>
|
||||
<p className="text-xl text-text-primary/80">This fox couldn't find what you're looking for.</p>
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="py-6 border-t border-accent-primary/10 text-center text-sm text-text-primary/60">
|
||||
<p>© 2023 - {new Date().getFullYear()} EndofTimee. All rights reserved.</p>
|
||||
<div className="flex justify-center items-center gap-2 mt-2">
|
||||
<span className="text-xs">Try the Konami code: ↑↑↓↓←→←→BA</span>
|
||||
<div className="bg-background-secondary px-2 py-0.5 rounded-full text-[10px] text-accent-primary">
|
||||
v1.3.0
|
||||
</div>
|
||||
</div>
|
||||
} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{isGameActive && <FoxGame />}
|
||||
</div>
|
||||
</Router>
|
||||
{/* Demo Switch Notification */}
|
||||
<SwitchNotification
|
||||
show={showInitialSwitchDemo}
|
||||
onClose={() => setShowInitialSwitchDemo(false)}
|
||||
alterName="Aurora"
|
||||
type="switch"
|
||||
autoClose
|
||||
autoCloseDelay={5000}
|
||||
/>
|
||||
|
||||
{/* Fox Game Overlay - Activated by Konami Code */}
|
||||
{isGameActive && (
|
||||
<>
|
||||
<FoxGame />
|
||||
<button
|
||||
onClick={() => setIsGameActive(false)}
|
||||
className="fixed top-4 right-4 z-[999] bg-red-500/80 hover:bg-red-500 px-3 py-1.5 rounded-md text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
Exit Game
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AuthChecker>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
|
|||
27
src/components/ProtectedRoute.tsx
Normal file
27
src/components/ProtectedRoute.tsx
Normal file
|
|
@ -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 <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
// If authenticated, render the protected content
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
172
src/components/SwitchNotification.tsx
Normal file
172
src/components/SwitchNotification.tsx
Normal file
|
|
@ -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<NodeJS.Timeout | null>(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: <AlertCircle className="text-yellow-400" size={20} />,
|
||||
bgColor: 'bg-yellow-500/20',
|
||||
borderColor: 'border-yellow-500/30',
|
||||
textColor: 'text-yellow-300'
|
||||
};
|
||||
case 'notice':
|
||||
return {
|
||||
icon: <AlertCircle className="text-blue-400" size={20} />,
|
||||
bgColor: 'bg-blue-500/20',
|
||||
borderColor: 'border-blue-500/30',
|
||||
textColor: 'text-blue-300'
|
||||
};
|
||||
case 'switch':
|
||||
default:
|
||||
return {
|
||||
icon: <RefreshCw className="text-accent-primary animate-spin" size={20} />,
|
||||
bgColor: 'bg-accent-primary/20',
|
||||
borderColor: 'border-accent-primary/30',
|
||||
textColor: 'text-accent-primary'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, bgColor, borderColor, textColor } = getIconAndStyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed bottom-4 right-4 z-50 max-w-xs w-full sm:w-auto transition-all duration-300 ${
|
||||
isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'
|
||||
}`}
|
||||
>
|
||||
<div className={`relative p-4 rounded-lg shadow-lg ${bgColor} border ${borderColor}`}>
|
||||
{/* Title row with icon, text and close button */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span className={`font-medium ${textColor}`}>
|
||||
{type === 'switch' ? 'System Switch Detected' : type === 'warning' ? 'System Warning' : 'System Notice'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-1 rounded-full hover:bg-background-primary/30 transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Message content */}
|
||||
<div className="text-sm text-text-primary/90">
|
||||
{type === 'switch' ? (
|
||||
<>Now fronting: <span className="font-medium">{selectedAlter}</span></>
|
||||
) : (
|
||||
message || 'System notification'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress bar for auto-close */}
|
||||
{autoClose && (
|
||||
<div className="w-full h-1 bg-background-primary/30 rounded-full mt-3 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-100 ${
|
||||
type === 'switch' ? 'bg-accent-primary' :
|
||||
type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500'
|
||||
}`}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SwitchNotification;
|
||||
271
src/components/SystemStatus.tsx
Normal file
271
src/components/SystemStatus.tsx
Normal file
|
|
@ -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<Date | null>(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 (
|
||||
<div className={`bg-background-secondary/30 rounded-lg p-3 shadow-sm ${className}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getCurrentSafetyLevel().color}`}></div>
|
||||
<span className="text-sm font-medium">System Status</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggleExpand}
|
||||
className="p-1 hover:bg-background-primary/30 rounded-full"
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Full or expanded minimal view
|
||||
return (
|
||||
<FoxCard className={className}>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="text-accent-primary" size={20} />
|
||||
<h3 className="font-medium">System Status</h3>
|
||||
</div>
|
||||
{minimal && (
|
||||
<button
|
||||
onClick={toggleExpand}
|
||||
className="p-1 hover:bg-background-primary/30 rounded-full"
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs opacity-70">
|
||||
{lastUpdate ? `Updated: ${lastUpdate.toLocaleTimeString()}` : 'Not updated'}
|
||||
</span>
|
||||
<div className={`ml-1 w-2 h-2 rounded-full ${isFresh ? 'bg-green-500' : 'bg-yellow-500'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Safety Level Selector */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={16} className="text-accent-primary" />
|
||||
<span className="text-sm font-medium">Safety Level:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{safetyLevels.map(level => (
|
||||
<button
|
||||
key={level.value}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mental State Selector */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain size={16} className="text-accent-primary" />
|
||||
<span className="text-sm font-medium">Mental State:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mentalStates.map(state => (
|
||||
<button
|
||||
key={state.value}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fronting Status Selector */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users size={16} className="text-accent-primary" />
|
||||
<span className="text-sm font-medium">Fronting Status:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{frontingStatuses.map(status => (
|
||||
<button
|
||||
key={status.value}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Fronters */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle size={16} className="text-accent-primary" />
|
||||
<span className="text-sm font-medium">Current Fronters:</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{systemMembers.slice(0, 3).map(member => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="px-3 py-1 rounded-full text-xs bg-background-primary/40 border border-accent-primary/20"
|
||||
style={{ borderColor: member.color }}
|
||||
>
|
||||
{member.name} ({member.role})
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="px-3 py-1 rounded-full text-xs bg-background-primary/30 border border-transparent hover:border-gray-600 flex items-center gap-1"
|
||||
>
|
||||
<RefreshCw size={12} />
|
||||
<span>Change</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isFresh && (
|
||||
<div className="mt-4 p-2 rounded bg-yellow-500/10 text-yellow-300 text-xs">
|
||||
Status was last updated over 5 minutes ago. It may not reflect current conditions.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpdating && (
|
||||
<div className="absolute inset-0 bg-background-primary/50 backdrop-blur-sm flex items-center justify-center rounded-xl">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-accent-primary border-t-transparent rounded-full"></div>
|
||||
</div>
|
||||
)}
|
||||
</FoxCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
|
|
@ -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<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();
|
||||
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 (
|
||||
<div className="min-h-screen w-full flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-6xl bg-background-primary/80 backdrop-blur-sm rounded-xl shadow-xl border border-accent-primary/20 overflow-hidden transition-all duration-300 hover:border-accent-neon/40 hover:shadow-accent-primary/20">
|
||||
<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} />
|
||||
|
|
@ -25,21 +82,41 @@ const VNCViewer = () => {
|
|||
</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={toggleFullscreen}
|
||||
className="p-2 rounded-lg hover:bg-accent-primary/20 transition-colors"
|
||||
title="Toggle Fullscreen"
|
||||
>
|
||||
<Monitor size={20} className="text-accent-primary" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => 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}
|
||||
>
|
||||
<Power
|
||||
size={20}
|
||||
className={isConnected ? "text-green-500" : "text-accent-primary"}
|
||||
className={isConnected ? "text-red-400" : "text-accent-primary"}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -49,25 +126,59 @@ const VNCViewer = () => {
|
|||
{isConnected ? (
|
||||
<iframe
|
||||
id="vnc-iframe"
|
||||
src="http://68.104.222.58:6080/vnc.html?host=68.104.222.58&port=5901&autoconnect=true&resize=scale"
|
||||
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-2 border-t border-accent-primary/20 flex justify-between items-center text-sm text-text-primary/60">
|
||||
<span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
<span>Press ESC to exit fullscreen</span>
|
||||
<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;
|
||||
export default VNCViewer;
|
||||
|
|
|
|||
164
src/context/AuthContext.tsx
Normal file
164
src/context/AuthContext.tsx
Normal file
|
|
@ -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<boolean>;
|
||||
logout: () => void;
|
||||
updateSystemState: (newState: Partial<SystemState>) => void;
|
||||
}
|
||||
|
||||
// System members data
|
||||
const systemMembers: SystemMember[] = [
|
||||
{ id: '1', name: 'Aurora', role: 'Host', color: '#9d4edd' },
|
||||
{ id: '2', name: 'Alex', role: 'Younger', color: '#4ea8de' },
|
||||
{ id: '3', name: 'Psy', role: 'Protector', color: '#5e548e' },
|
||||
{ id: '4', name: 'Xander', role: 'Caretaker', color: '#219ebc' },
|
||||
{ id: '5', name: 'Unknown', role: 'Fragment', color: '#6c757d' },
|
||||
{ id: '6', name: 'The thing', role: 'Persecutor', color: '#e63946' },
|
||||
{ id: '7', name: 'Unknown 2', role: 'Fragment', color: '#6c757d' },
|
||||
];
|
||||
|
||||
// Creating the context with a default value of null
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
// Initialize authentication state from localStorage if available
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
|
||||
const stored = localStorage.getItem('isAuthenticated');
|
||||
return stored === 'true';
|
||||
});
|
||||
|
||||
const [username, setUsername] = useState<string | null>(() => {
|
||||
return localStorage.getItem('username');
|
||||
});
|
||||
|
||||
// Initialize system state from localStorage or set defaults
|
||||
const [systemState, setSystemState] = useState<SystemState | null>(() => {
|
||||
const stored = localStorage.getItem('systemState');
|
||||
if (stored && isAuthenticated) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
return isAuthenticated ? {
|
||||
safetyLevel: 'safe',
|
||||
mentalState: 'ok',
|
||||
frontingStatus: 'single',
|
||||
currentFronters: [systemMembers[0]] // Default to Aurora as fronter
|
||||
} : null;
|
||||
});
|
||||
|
||||
// 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<SystemState>) => {
|
||||
if (!systemState) return;
|
||||
|
||||
const updatedState = { ...systemState, ...newState };
|
||||
setSystemState(updatedState);
|
||||
localStorage.setItem('systemState', JSON.stringify(updatedState));
|
||||
};
|
||||
|
||||
// Construct the context value
|
||||
const contextValue: AuthContextType = {
|
||||
isAuthenticated,
|
||||
username,
|
||||
systemState,
|
||||
login,
|
||||
logout,
|
||||
updateSystemState
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook for easier context consumption
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Export system members data for use in other components
|
||||
export { systemMembers };
|
||||
|
|
@ -20,7 +20,7 @@ const AboutPage = () => {
|
|||
<FoxCard className="header-card">
|
||||
<h1 className="text-glow">About Me</h1>
|
||||
<p className="text-gradient">
|
||||
Transfem Foxgirl • {age} years old • Programmer & Streamer
|
||||
End • ProtoFoxes • They/Them • {age} years old • Programmer & Streamer
|
||||
</p>
|
||||
</FoxCard>
|
||||
|
||||
|
|
|
|||
226
src/pages/LoginPage.tsx
Normal file
226
src/pages/LoginPage.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="page-container">
|
||||
<FoxCard className="header-card mb-8">
|
||||
<h1 className="text-glow">System Access</h1>
|
||||
<p className="text-gradient">Please authenticate to view system information</p>
|
||||
</FoxCard>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<FoxCard className="w-full max-w-md p-8 relative overflow-hidden">
|
||||
{/* Decorative fox ears in corners */}
|
||||
<div className="absolute -top-4 -left-4 w-8 h-8 rounded-br-xl bg-fox-pink opacity-20 transform rotate-45"></div>
|
||||
<div className="absolute -top-4 -right-4 w-8 h-8 rounded-bl-xl bg-fox-pink opacity-20 transform -rotate-45"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-primary/20 flex items-center justify-center">
|
||||
<Brain size={32} className="text-accent-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-bold text-glow mb-6 text-center">System Authentication</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/20 text-red-300 p-3 rounded-md mb-4 flex items-start gap-2">
|
||||
<AlertTriangle size={18} className="mt-0.5 flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLocked && (
|
||||
<div className="bg-orange-500/20 text-orange-300 p-3 rounded-md mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Lock size={18} />
|
||||
<span className="font-medium">Access Temporarily Locked</span>
|
||||
</div>
|
||||
<p className="text-sm">
|
||||
Please wait {lockCountdown} seconds before trying again.
|
||||
</p>
|
||||
<div className="w-full h-1.5 bg-background-primary rounded-full mt-2 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-orange-500 transition-all duration-1000"
|
||||
style={{ width: `${(lockCountdown / 30) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="username" className="block text-sm font-medium">
|
||||
System Username
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<User size={18} className="text-accent-primary/60" />
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full pl-10 p-3 bg-background-secondary/50 border border-accent-primary/20 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-neon/40 focus:border-accent-neon transition-colors"
|
||||
placeholder="Enter username"
|
||||
required
|
||||
disabled={isLoading || isLocked}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="password" className="block text-sm font-medium">
|
||||
Access Code
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<Lock size={18} className="text-accent-primary/60" />
|
||||
</div>
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full pl-10 p-3 bg-background-secondary/50 border border-accent-primary/20 rounded-md focus:outline-none focus:ring-2 focus:ring-accent-neon/40 focus:border-accent-neon transition-colors"
|
||||
placeholder="Enter access code"
|
||||
required
|
||||
disabled={isLoading || isLocked}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-accent-primary/60 hover:text-accent-primary"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full p-3 flex items-center justify-center gap-2 bg-accent-primary hover:bg-accent-neon transition-colors rounded-md text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={isLoading || isLocked}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" />
|
||||
<span>Validating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn size={18} />
|
||||
<span>Access System</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-center text-sm mt-6">
|
||||
<div className="flex items-center justify-center gap-2 mb-2 text-accent-primary">
|
||||
<Shield size={16} />
|
||||
<span className="font-medium">Secure Access Only</span>
|
||||
</div>
|
||||
<p className="text-text-primary/60">
|
||||
This area contains private system information.
|
||||
</p>
|
||||
<p className="mt-2 text-accent-primary/80">
|
||||
{locationState?.from ? 'You must log in to access the requested page.' : 'Access credentials are required.'}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 p-2 border border-accent-primary/10 rounded-md bg-background-secondary/20">
|
||||
<p className="text-xs text-text-primary/70">
|
||||
Available users: "system", "aurora", or "endoftimee"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</FoxCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
235
src/pages/SystemPage.tsx
Normal file
235
src/pages/SystemPage.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="page-container">
|
||||
<FoxCard className="header-card">
|
||||
<h1 className="text-glow">Our System</h1>
|
||||
<p className="text-gradient">
|
||||
EndofTimee System • 7 Members • Est. May 15th, 2009
|
||||
</p>
|
||||
<div className="flex justify-center mt-4 gap-2">
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Info size={16} />
|
||||
<span>{showFrontingInfo ? 'Hide Fronting Info' : 'Show Fronting Info'}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 bg-accent-primary/20 hover:bg-accent-primary/40 rounded-md transition-colors flex items-center gap-2"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
<span>Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</FoxCard>
|
||||
|
||||
<div className="content-grid">
|
||||
<FoxCard>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Users size={24} className="text-accent-primary" />
|
||||
<h2 className="text-xl font-semibold">About Our System</h2>
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p className="mt-4">
|
||||
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.
|
||||
</p>
|
||||
|
||||
{showFrontingInfo && (
|
||||
<div className="mt-6 p-4 border border-accent-primary/30 rounded-lg bg-background-secondary/20 animate-fade-in">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity size={18} className="text-accent-primary" />
|
||||
<span className="font-medium">Current Fronting Status</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
|
||||
<div className="p-3 bg-background-primary/40 rounded-lg">
|
||||
<span className="block font-medium mb-1">Safety Level:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-green-500"></span>
|
||||
<span>Safe</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-background-primary/40 rounded-lg">
|
||||
<span className="block font-medium mb-1">Mental State:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-yellow-500"></span>
|
||||
<span>OK Enough</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-background-primary/40 rounded-lg">
|
||||
<span className="block font-medium mb-1">Interaction:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-blue-400"></span>
|
||||
<span>Ask before touching</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-background-primary/40 rounded-lg">
|
||||
<span className="block font-medium mb-1">Front Status:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-3 h-3 rounded-full bg-purple-400"></span>
|
||||
<span>Cofronting</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</FoxCard>
|
||||
|
||||
<FoxCard>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Heart size={24} className="text-accent-primary" />
|
||||
<h2 className="text-xl font-semibold">Our Members</h2>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
<li className="p-3 bg-background-secondary/20 rounded-lg">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium text-accent-neon">Aurora</div>
|
||||
<div className="text-xs bg-accent-primary/20 px-2 py-1 rounded-full">Host</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-80 mt-1">Born May 15th, 2009 • 15 years old • She/her</div>
|
||||
<div className="text-sm mt-2">Chaotic foxgirl who enjoys programming, talking with Alice, and cheese. Dislikes loud noise and math.</div>
|
||||
</li>
|
||||
|
||||
<li className="p-3 bg-background-secondary/20 rounded-lg">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium text-accent-neon">Alex</div>
|
||||
<div className="text-xs bg-accent-primary/20 px-2 py-1 rounded-full">Younger</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-80 mt-1">Arrived December 14th, 2023 • Around 10? • Alex/she/her</div>
|
||||
<div className="text-sm mt-2">Younger alter who appears when Aurora feels like it. Refers to herself in the third person.</div>
|
||||
</li>
|
||||
|
||||
<li className="p-3 bg-background-secondary/20 rounded-lg">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium text-accent-neon">Psy</div>
|
||||
<div className="text-xs bg-accent-primary/20 px-2 py-1 rounded-full">Protector</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-80 mt-1">Arrived December 31st, 2024 • Unknown age • They/them</div>
|
||||
<div className="text-sm mt-2">System protector who appears during high anxiety or stress. Very calm and logical. Helps stabilize the system.</div>
|
||||
</li>
|
||||
|
||||
<li className="p-3 bg-background-secondary/20 rounded-lg">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium text-accent-neon">Xander</div>
|
||||
<div className="text-xs bg-accent-primary/20 px-2 py-1 rounded-full">Caretaker</div>
|
||||
</div>
|
||||
<div className="text-sm opacity-80 mt-1">Arrived December 16th, 2024 • Unknown age • They/them</div>
|
||||
<div className="text-sm mt-2">Older brother vibes. Calm and collected. Helps maintain system stability.</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-4 p-3 border border-red-500/30 rounded-lg bg-red-900/10">
|
||||
<div className="flex items-center gap-2 text-red-400">
|
||||
<AlertTriangle size={18} />
|
||||
<span className="font-medium">Important Safety Notice</span>
|
||||
</div>
|
||||
<p className="text-sm mt-2">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.</p>
|
||||
</div>
|
||||
</FoxCard>
|
||||
|
||||
<FoxCard>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Brain size={24} className="text-accent-primary" />
|
||||
<h2 className="text-xl font-semibold">Communication Guide</h2>
|
||||
</div>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 border border-accent-primary/20 rounded-lg bg-background-secondary/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Shield size={18} className="text-accent-primary" />
|
||||
<span className="font-medium">Interaction Guidelines</span>
|
||||
</div>
|
||||
<ul className="text-sm space-y-2 mt-3">
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={14} className="text-accent-primary" />
|
||||
<span>Always ask before physical contact</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={14} className="text-accent-primary" />
|
||||
<span>Address whoever is fronting by their name</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={14} className="text-accent-primary" />
|
||||
<span>Respect boundaries indicated by front status</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Tag size={14} className="text-accent-primary" />
|
||||
<span>Be patient during switches or blurry fronting</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-accent-primary/20 rounded-lg bg-background-secondary/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar size={18} className="text-accent-primary" />
|
||||
<span className="font-medium">System Timeline</span>
|
||||
</div>
|
||||
<ul className="text-sm space-y-2 mt-3">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-accent-primary mt-1.5"></span>
|
||||
<div>
|
||||
<span className="font-medium">May 15th, 2009</span>
|
||||
<p className="opacity-80">System formation (Aurora's arrival)</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-accent-primary mt-1.5"></span>
|
||||
<div>
|
||||
<span className="font-medium">November 19th, 2011</span>
|
||||
<p className="opacity-80">Traumatic experience, new fragment</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-accent-primary mt-1.5"></span>
|
||||
<div>
|
||||
<span className="font-medium">December 2023 - Present</span>
|
||||
<p className="opacity-80">Period of system growth and discovery</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 border border-accent-primary/20 rounded-lg bg-background-secondary/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Shield size={18} className="text-accent-primary" />
|
||||
<span className="font-medium">A note on our privacy</span>
|
||||
</div>
|
||||
<p className="text-sm opacity-80">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</FoxCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemPage;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue