Merge pull request #1 from EndofTimee/development

Development
This commit is contained in:
Unknown 2025-03-07 18:10:03 -07:00 committed by GitHub
commit 2b58241c82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1604 additions and 372 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View 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;

View 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;

View 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;

View file

@ -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
View 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 };

View file

@ -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
View 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
View 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;

View file

@ -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;
}
}