iamfebin's picture
feat: implement sandbox mode
a95fdd1
Raw
History Blame Contribute Delete
51.9 kB
import { useEffect, useState } from 'react';
import { supabase } from './supabaseClient';
import Auth from './components/Auth';
import ReviewQueue from './components/ReviewQueue';
import JourneyPath from './components/JourneyPath';
import WindowShell from './components/WindowShell';
import TerminalEmulator from './components/TerminalEmulator';
import PythonChallenge from './components/PythonChallenge';
import MySQLPlayground from './components/MySQLPlayground';
import FileExplorer from './components/FileExplorer';
import Settings from './components/Settings';
interface WindowState {
isOpen: boolean;
isMinimized: boolean;
}
export default function App() {
const [user, setUser] = useState<any>(null);
const [overrideProblemId, setOverrideProblemId] = useState<string | null>(null);
// Window states
const [windows, setWindows] = useState<Record<string, WindowState>>({
queue: { isOpen: true, isMinimized: false },
journey: { isOpen: false, isMinimized: false },
terminal: { isOpen: false, isMinimized: false },
computer: { isOpen: false, isMinimized: false },
sync: { isOpen: false, isMinimized: false },
python: { isOpen: false, isMinimized: false },
mysql: { isOpen: false, isMinimized: false },
settings: { isOpen: false, isMinimized: false },
});
const [activeWindow, setActiveWindow] = useState<string>('queue');
const [isMobile, setIsMobile] = useState(false);
// Start Menu and System info
const [isStartOpen, setIsStartOpen] = useState(false);
const [streak, setStreak] = useState<any>(null);
const [time, setTime] = useState('');
// Sync state
const [syncing, setSyncing] = useState(false);
const [syncResult, setSyncResult] = useState<string | null>(null);
// Wallpaper state
const [wallpaperUrl, setWallpaperUrl] = useState<string>(() => {
return localStorage.getItem('desktopWallpaperUrl') || 'https://lh3.googleusercontent.com/aida-public/AB6AXuAMJHVhhK8_G7F8sbhn8F7w4AZWXb1O_-HLKvrZ_fJlbUVMcVuxxEOO_LlOh8qIwtAn2KvzvCwzmtNLLEZ5uYkCEsx8ZWXJR609qgSMRSX8LBBeskk4VzVXnyzsgIKCedeV2PvGxJlUNnledcWKCXqG9egQi8dgTcA7C2z82QyM73KZ4s7ZRzZHNupuQt1ocfcMl9E_x1PWFRridl751LIpyGZgednS5CmVw2rZvFc_tbp2QTxgVHJ_59myEBmy6aajbO06AhkKmCvI';
});
// Context menu state
const [contextMenu, setContextMenu] = useState<{ visible: boolean; x: number; y: number }>({
visible: false,
x: 0,
y: 0
});
// Fullscreen state
const [isFullscreen, setIsFullscreen] = useState(false);
// Fetch streak info
const fetchStreak = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/streak`, {
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
setStreak(data);
}
} catch (err) {
console.error('Error fetching streak:', err);
}
};
useEffect(() => {
// Check initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
});
// Listen for auth state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null);
});
// Clock Updater
const updateTime = () => {
const date = new Date();
let hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // 0 is 12
const minStr = minutes < 10 ? '0' + minutes : minutes;
setTime(`${hours}:${minStr} ${ampm}`);
};
updateTime();
const interval = setInterval(updateTime, 30000);
return () => {
subscription.unsubscribe();
clearInterval(interval);
};
}, []);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => {
window.removeEventListener('resize', checkMobile);
};
}, []);
useEffect(() => {
if (user) {
fetchStreak();
const savedWallpaper = localStorage.getItem('desktopWallpaperUrl');
if (savedWallpaper) {
setWallpaperUrl(savedWallpaper);
}
}
}, [user]);
// Fullscreen change listener
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
// Zoom prevention listener
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey) {
e.preventDefault();
}
};
const handleKeydown = (e: KeyboardEvent) => {
if (e.ctrlKey && (
e.key === '=' ||
e.key === '-' ||
e.key === '0' ||
e.key === '+' ||
e.code === 'NumpadAdd' ||
e.code === 'NumpadSubtract'
)) {
e.preventDefault();
}
};
const handleTouchmove = (e: TouchEvent) => {
if (e.touches.length > 1) {
e.preventDefault();
}
};
document.addEventListener('wheel', handleWheel, { passive: false });
document.addEventListener('keydown', handleKeydown);
document.addEventListener('touchmove', handleTouchmove, { passive: false });
return () => {
document.removeEventListener('wheel', handleWheel);
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('touchmove', handleTouchmove);
};
}, []);
const handleContextMenu = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
// Prevent menu if clicking interactive windows/buttons/inputs
if (
target.closest('.window-shadow') ||
target.closest('button') ||
target.closest('input') ||
target.closest('textarea') ||
target.closest('details') ||
target.closest('#start-menu')
) {
return;
}
e.preventDefault();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY
});
};
const closeContextMenu = () => {
if (contextMenu.visible) {
setContextMenu(prev => ({ ...prev, visible: false }));
}
};
const toggleFullscreen = () => {
closeContextMenu();
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch((err) => {
console.error(`Error enabling fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
};
const compressAndSaveWallpaper = (file: File) => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
const MAX_WIDTH = 1920;
const MAX_HEIGHT = 1080;
if (width > MAX_WIDTH || height > MAX_HEIGHT) {
if (width / height > MAX_WIDTH / MAX_HEIGHT) {
height = Math.round((height * MAX_WIDTH) / width);
width = MAX_WIDTH;
} else {
width = Math.round((width * MAX_HEIGHT) / height);
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0, width, height);
try {
const dataUrl = canvas.toDataURL('image/jpeg', 0.7);
setWallpaperUrl(dataUrl);
localStorage.setItem('desktopWallpaperUrl', dataUrl);
localStorage.setItem('customWallpaperUrl', dataUrl);
localStorage.setItem('customWallpaperName', file.name);
} catch (err) {
console.error('Failed to save compressed image to localStorage:', err);
// Fallback: set it as state but warn
const rawDataUrl = event.target?.result as string;
setWallpaperUrl(rawDataUrl);
}
} else {
const rawDataUrl = event.target?.result as string;
setWallpaperUrl(rawDataUrl);
try {
localStorage.setItem('desktopWallpaperUrl', rawDataUrl);
localStorage.setItem('customWallpaperUrl', rawDataUrl);
localStorage.setItem('customWallpaperName', file.name);
} catch (err) {
console.error('Failed to save raw image to localStorage:', err);
}
}
};
img.src = event.target?.result as string;
};
reader.readAsDataURL(file);
};
const handleWallpaperChange = (e: React.ChangeEvent<HTMLInputElement>) => {
closeContextMenu();
const file = e.target.files?.[0];
if (file) {
compressAndSaveWallpaper(file);
}
};
const openWindow = (id: string) => {
setWindows(prev => ({
...prev,
[id]: { isOpen: true, isMinimized: false }
}));
setActiveWindow(id);
setIsStartOpen(false);
};
const closeWindow = (id: string) => {
setWindows(prev => ({
...prev,
[id]: { ...prev[id], isOpen: false }
}));
};
const minimizeWindow = (id: string) => {
setWindows(prev => ({
...prev,
[id]: { ...prev[id], isMinimized: true }
}));
};
const toggleWindowMinimize = (id: string) => {
setWindows(prev => {
const win = prev[id];
if (win.isMinimized || !win.isOpen) {
setActiveWindow(id);
return {
...prev,
[id]: { isOpen: true, isMinimized: false }
};
} else if (activeWindow === id) {
return {
...prev,
[id]: { ...win, isMinimized: true }
};
} else {
setActiveWindow(id);
return prev;
}
});
};
const handleSelectProblemForReview = (problemId: string) => {
setOverrideProblemId(problemId);
openWindow('queue');
};
const triggerSync = async () => {
openWindow('sync');
setSyncing(true);
setSyncResult(null);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/sync`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
setSyncResult(data.message || 'Synchronization successfully completed!');
fetchStreak();
} else {
setSyncResult('Sync operation failed at server host.');
}
} catch {
setSyncResult('Cloud synchronization server unreachable.');
} finally {
setSyncing(false);
}
};
const handleSignOut = async () => {
setIsStartOpen(false);
await supabase.auth.signOut();
};
if (!user) {
return <Auth />;
}
const username = user.email ? user.email.split('@')[0] : 'Player_One';
const isAnyWindowOpen = Object.values(windows).some(w => w.isOpen && !w.isMinimized);
return (
<div
onContextMenu={handleContextMenu}
onClick={closeContextMenu}
className="w-screen h-screen m-0 p-0 relative font-sans text-ink-black overflow-hidden select-none"
>
{/* Background Wallpaper image */}
<div
className="absolute inset-0 w-full h-full bg-cover bg-center z-[-1] saturate-150 contrast-125 transition-all duration-300"
style={{ backgroundImage: `url('${wallpaperUrl}')` }}
/>
{/* Desktop Icons column */}
{!isMobile && (
<nav className="fixed left-0 top-12 h-[calc(100vh-80px)] w-[140px] flex flex-col items-center py-gutter-md space-y-6 z-10 overflow-y-auto custom-scrollbar select-none">
{/* Daily Queue Shortcut */}
<button
onClick={() => toggleWindowMinimize('queue')}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-secondary-container transition-colors shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]">
<span className="material-symbols-outlined text-4xl" style={{ fontVariationSettings: "'FILL' 1" }}>
view_list
</span>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#fde047] uppercase">
DAILY<br/>QUEUE
</span>
</button>
{/* Journey Path Shortcut */}
<button
onClick={() => toggleWindowMinimize('journey')}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-secondary-container transition-colors shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]">
<span className="material-symbols-outlined text-4xl" style={{ fontVariationSettings: "'FILL' 1" }}>
route
</span>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#fde047] uppercase">
JOURNEY<br/>PATH
</span>
</button>
{/* Drive Sync Shortcut */}
<button
onClick={triggerSync}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-secondary-container transition-colors shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]">
<span className="material-symbols-outlined text-4xl" style={{ fontVariationSettings: "'FILL' 1" }}>
cloud_sync
</span>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#fde047] uppercase">
DRIVE<br/>SYNC
</span>
</button>
{/* My Computer Shortcut */}
<button
onClick={() => toggleWindowMinimize('computer')}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-secondary-container transition-colors shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)] p-1 overflow-hidden">
<img
alt="My Computer"
className="w-full h-full object-cover"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuD2tUGA2d7QiNyajDAWYK183zhNTgtAZpOXK3E9wRvHGs-t6ykWt9uPScYe_fziWzQI0pREcY_ThI341WvGMusyYmnAkagfuwx6wubIs1ES68DO8CCNAlcHcb2zOUU4MJeYuhWDy1uYRqyGYjIaDUqfgNWk2vm4WzwRqoorn2dxtZ6QdJhEKbXjG8fG9chkkvr68qJf0fLUL6bIqBXZg2FJ30S7zS3oZ4IZsug-wLyRYuJ4Gu_86snNUo1whNrBdZm5OMREfc9sLbwJ"
/>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#fde047] uppercase">
MY<br/>COMP
</span>
</button>
{/* Terminal Shortcut */}
<button
onClick={() => toggleWindowMinimize('terminal')}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-secondary-container transition-colors shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)] p-1 overflow-hidden">
<img
alt="Terminal"
className="w-full h-full object-cover"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuA2AvcQ2oKWP8AJ8yR5nF82LLDEH1FKySUK-npnqEjvJ6Cs9xp8joCwH7x43oTCAC5xhO2rQdYzka3B6u72FNbdxMnsv3WJLye3bKochu8AnnRlS3szfYSaj724sYsiZo4n8G-oWqvl7C0rOGaDaU2nRH1xXmW2PBi6L2NryTsUaTCVZQKTKBBNNEOig5kh7B52e-iIuj2PTzaSjF9ebp6gMuCnBtJT8Sxo5Qsmvfm-t2U5r_qQpRiYj--Ut41T7NTh-tHgs4S4kBvz"
/>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#fde047] uppercase">
TERMINAL
</span>
</button>
{localStorage.getItem('algospaced_sandbox_mode') === 'true' && (
<button
onClick={() => toggleWindowMinimize('settings')}
className="flex flex-col items-center justify-center text-ink-black gap-2.5 p-2 w-full group icon-hover-shift cursor-pointer outline-none animate-fade-in"
>
<div className="w-16 h-16 bg-paper-white border-[3px] border-ink-black flex items-center justify-center gadget-shadow group-hover:bg-[#10b981] group-hover:text-white transition-all shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]">
<span className="material-symbols-outlined text-4xl" style={{ fontVariationSettings: "'FILL' 1" }}>
key
</span>
</div>
<span className="font-icon-label text-[9px] bg-ink-black text-white px-2 py-1 border-[3px] border-ink-black text-center leading-tight shadow-[2px_2px_0px_0px_#10b981] uppercase">
API<br/>KEYS
</span>
</button>
)}
</nav>
)}
{/* System Gadget Widget (Top Right) */}
{!isMobile && (
<div className="absolute top-desktop-margin right-desktop-margin w-64 bg-paper-white border-[3px] border-ink-black gadget-shadow z-20 overflow-hidden flex flex-col shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)] select-none">
<div className="bg-[#0ea5e9] border-b-[3px] border-ink-black px-3 py-1 flex justify-between items-center text-white">
<span className="font-window-title text-xs font-bold tracking-wide">Profile.sys</span>
</div>
<div className="p-3 flex items-center gap-3 bg-[#fdf8e1]">
<div className="w-12 h-12 border-[3px] border-ink-black overflow-hidden bg-primary-fixed-dim shadow-[2px_2px_0_0_#1E293B] shrink-0">
<img
className="w-full h-full object-cover"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBl1MnAmh_W_EWibibrGw1F1YSenuKlaIn3JymUnBKn1nPLjAaOBjoLzdT5krzVDmihFhXOk-DEtNTdM2Js1_Ttr--5WZohhyqD1QfpS6j_yU1_-4wweOmgWq2HLUYNMf0mzQDNLNu5KbACEJKL5sm1YaTLPePlO687X3VHYsUhWTpUQeBwFjushaBpyW8tEa2dJj5ZSRWq0qa5GhtE0zAAW741e2SIRtFOp7CvBYEEetymSiucK513lli1TUwgWIgXvlfxZoGbH00q"
/>
</div>
<div className="overflow-hidden">
<div className="font-headline-md text-sm font-bold text-ink-black truncate">{username}</div>
<div className="flex items-center gap-1 text-red-500 mt-1 select-none font-bold">
<span className="material-symbols-outlined text-sm" style={{ fontVariationSettings: "'FILL' 1" }}>
local_fire_department
</span>
<span className="font-arcade text-[7px]">{streak?.current_streak ?? 0} DAY STREAK</span>
</div>
</div>
</div>
</div>
)}
{/* Windows Phone Metro Start Screen (Mobile Home View) */}
{isMobile && !isAnyWindowOpen && (
<div className="w-full h-[calc(100vh-56px)] overflow-y-auto custom-scrollbar p-6 pb-24 flex flex-col gap-6 z-10 relative">
{/* Metro Header */}
<div className="flex flex-col select-none border-b-[3px] border-dashed border-ink-black pb-4">
<span className="text-[9px] font-arcade text-trash-gray font-bold uppercase tracking-wider">AlgoSpaced OS v1.0</span>
<h1 className="text-4xl font-extrabold font-headline-md text-ink-black uppercase tracking-tight mt-1">start</h1>
</div>
{/* Tiles Grid */}
<div className="grid grid-cols-2 gap-4">
{/* Profile Tile (Wide - 2 cols) */}
<div className="col-span-2 bg-[#fdf8e1] border-[3px] border-ink-black p-4 flex items-center gap-4 gadget-shadow shadow-[inset_2px_2px_0px_rgba(255,255,255,0.8)]">
<div className="w-12 h-12 border-[3px] border-ink-black overflow-hidden bg-primary-fixed-dim shadow-[2px_2px_0_0_#1E293B] shrink-0">
<img
className="w-full h-full object-cover"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuBl1MnAmh_W_EWibibrGw1F1YSenuKlaIn3JymUnBKn1nPLjAaOBjoLzdT5krzVDmihFhXOk-DEtNTdM2Js1_Ttr--5WZohhyqD1QfpS6j_yU1_-4wweOmgWq2HLUYNMf0mzQDNLNu5KbACEJKL5sm1YaTLPePlO687X3VHYsUhWTpUQeBwFjushaBpyW8tEa2dJj5ZSRWq0qa5GhtE0zAAW741e2SIRtFOp7CvBYEEetymSiucK513lli1TUwgWIgXvlfxZoGbH00q"
/>
</div>
<div className="overflow-hidden">
<div className="font-headline-md text-sm font-bold text-ink-black truncate">{username}</div>
<div className="flex items-center gap-1 text-red-500 mt-1 select-none font-bold">
<span className="material-symbols-outlined text-sm" style={{ fontVariationSettings: "'FILL' 1" }}>
local_fire_department
</span>
<span className="font-arcade text-[7px]">{streak?.current_streak ?? 0} DAY STREAK</span>
</div>
</div>
</div>
{/* Daily Queue Tile */}
<button
onClick={() => toggleWindowMinimize('queue')}
className="bg-grass-green text-ink-black border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<span className="material-symbols-outlined text-3xl align-top" style={{ fontVariationSettings: "'FILL' 1" }}>
view_list
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
DAILY<br/>QUEUE
</span>
</button>
{/* Journey Path Tile */}
<button
onClick={() => toggleWindowMinimize('journey')}
className="bg-[#0ea5e9] text-white border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<span className="material-symbols-outlined text-3xl align-top" style={{ fontVariationSettings: "'FILL' 1" }}>
route
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
JOURNEY<br/>PATH
</span>
</button>
{/* My Computer Tile */}
<button
onClick={() => toggleWindowMinimize('computer')}
className="bg-paper-white text-ink-black border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<img
alt="My Computer"
className="w-10 h-10 object-cover border-2 border-ink-black bg-white"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuD2tUGA2d7QiNyajDAWYK183zhNTgtAZpOXK3E9wRvHGs-t6ykWt9uPScYe_fziWzQI0pREcY_ThI341WvGMusyYmnAkagfuwx6wubIs1ES68DO8CCNAlcHcb2zOUU4MJeYuhWDy1uYRqyGYjIaDUqfgNWk2vm4WzwRqoorn2dxtZ6QdJhEKbXjG8fG9chkkvr68qJf0fLUL6bIqBXZg2FJ30S7zS3oZ4IZsug-wLyRYuJ4Gu_86snNUo1whNrBdZm5OMREfc9sLbwJ"
/>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
MY COMP
</span>
</button>
{/* Terminal Tile */}
<button
onClick={() => toggleWindowMinimize('terminal')}
className="bg-[#1e293b] text-[#39ff14] border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<img
alt="Terminal"
className="w-10 h-10 object-cover border-2 border-ink-black bg-white"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuA2AvcQ2oKWP8AJ8yR5nF82LLDEH1FKySUK-npnqEjvJ6Cs9xp8joCwH7x43oTCAC5xhO2rQdYzka3B6u72FNbdxMnsv3WJLye3bKochu8AnnRlS3szfYSaj724sYsiZo4n8G-oWqvl7C0rOGaDaU2nRH1xXmW2PBi6L2NryTsUaTCVZQKTKBBNNEOig5kh7B52e-iIuj2PTzaSjF9ebp6gMuCnBtJT8Sxo5Qsmvfm-t2U5r_qQpRiYj--Ut41T7NTh-tHgs4S4kBvz"
/>
<span className="font-arcade text-[8px] text-[#39ff14] font-bold tracking-tight uppercase leading-tight">
TERMINAL
</span>
</button>
{/* Python Challenge Tile */}
<button
onClick={() => toggleWindowMinimize('python')}
className="bg-[#9d4edd] text-white border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<span className="material-symbols-outlined text-3xl align-top" style={{ fontVariationSettings: "'FILL' 1" }}>
sports_esports
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
PYTHON<br/>PLAY
</span>
</button>
{/* MySQL Playground Tile */}
<button
onClick={() => toggleWindowMinimize('mysql')}
className="bg-[#ffcc00] text-ink-black border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<span className="material-symbols-outlined text-3xl align-top" style={{ fontVariationSettings: "'FILL' 1" }}>
database
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
MYSQL<br/>PLAY
</span>
</button>
{localStorage.getItem('algospaced_sandbox_mode') === 'true' && (
<button
onClick={() => toggleWindowMinimize('settings')}
className="bg-[#10b981] text-white border-[3px] border-ink-black p-4 flex flex-col justify-between aspect-square gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<span className="material-symbols-outlined text-3xl align-top" style={{ fontVariationSettings: "'FILL' 1" }}>
key
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase leading-tight">
API<br/>KEYS
</span>
</button>
)}
{/* Drive Sync Tile (Wide - 2 cols) */}
<button
onClick={triggerSync}
className="col-span-2 bg-[#ffe24c] hover:bg-yellow-300 text-ink-black border-[3px] border-ink-black p-4 flex items-center justify-between gadget-shadow active:translate-y-1 active:translate-x-1 active:shadow-none select-none text-left cursor-pointer transition-all"
>
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-3xl" style={{ fontVariationSettings: "'FILL' 1" }}>
cloud_sync
</span>
<span className="font-arcade text-[8px] font-bold tracking-tight uppercase">
DRIVE NOTES SYNC
</span>
</div>
<span className="material-symbols-outlined text-lg">chevron_right</span>
</button>
</div>
</div>
)}
{/* Floating Windows Shell Containers */}
{/* 1. Daily Review Queue Window */}
<WindowShell
id="queue"
title="Daily Queue.exe"
icon="terminal"
isOpen={windows.queue.isOpen}
isMinimized={windows.queue.isMinimized}
onClose={() => closeWindow('queue')}
onMinimize={() => minimizeWindow('queue')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="750px"
defaultHeight="600px"
isMobile={isMobile}
>
<ReviewQueue
overrideProblemId={overrideProblemId}
onClearOverride={() => setOverrideProblemId(null)}
/>
</WindowShell>
{/* 2. Journey Path Map Window */}
<WindowShell
id="journey"
title="Journey_Path.exe"
icon="route"
isOpen={windows.journey.isOpen}
isMinimized={windows.journey.isMinimized}
onClose={() => closeWindow('journey')}
onMinimize={() => minimizeWindow('journey')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="700px"
defaultHeight="560px"
isMobile={isMobile}
>
<JourneyPath
onSelectProblemForReview={handleSelectProblemForReview}
/>
</WindowShell>
{/* 3. Command Line Terminal Window */}
<WindowShell
id="terminal"
title="Arcade Terminal.com"
icon="terminal"
isOpen={windows.terminal.isOpen}
isMinimized={windows.terminal.isMinimized}
onClose={() => closeWindow('terminal')}
onMinimize={() => minimizeWindow('terminal')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="640px"
defaultHeight="450px"
titleBarColor="#1e293b"
isMobile={isMobile}
>
<TerminalEmulator
onOpenWindow={openWindow}
onClose={() => closeWindow('terminal')}
/>
</WindowShell>
{/* 4. Google Drive Sync Dialog Window */}
<WindowShell
id="sync"
title="Drive_Sync.sys"
icon="cloud_sync"
isOpen={windows.sync.isOpen}
isMinimized={windows.sync.isMinimized}
onClose={() => closeWindow('sync')}
onMinimize={() => minimizeWindow('sync')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="480px"
defaultHeight="320px"
titleBarColor="#855400"
isMobile={isMobile}
>
<div className="w-full h-full bg-[#fdf8e1] p-6 flex flex-col justify-between">
<div className="space-y-4">
<div className="flex items-center gap-3 border-b-2 border-dashed border-ink-black pb-3">
<span className="material-symbols-outlined text-4xl text-[#855400] animate-bounce">cloud_sync</span>
<div>
<h4 className="font-arcade text-[10px] font-bold text-ink-black">DRIVE NOTES SYNCHRONIZATION</h4>
<span className="text-[8px] font-arcade text-trash-gray font-bold uppercase mt-1 block">Importing LeetCode sheets</span>
</div>
</div>
{syncing ? (
<div className="space-y-3 py-2">
<span className="text-[9px] font-arcade text-ink-black font-bold block animate-pulse">SYNC PROCESS RUNNING...</span>
<div className="w-full h-6 bg-white border-[3px] border-ink-black relative overflow-hidden select-none">
<div className="absolute inset-y-0 left-0 bg-[#ffe24c] border-r-2 border-ink-black w-[75%] animate-pulse" />
</div>
<p className="text-[9px] font-sans text-trash-gray leading-normal font-semibold">
Connecting to Supabase partition, requesting Google Drive manifest, indexing note items...
</p>
</div>
) : (
<div className="p-3 bg-white border-[3px] border-ink-black text-xs font-semibold text-ink-black leading-relaxed shadow-[2px_2px_0px_0px_#1E293B]">
{syncResult || 'Idle. Ready to synchronize Leitner box data logs from Sheets drive storage.'}
</div>
)}
</div>
<div className="flex gap-3 justify-end pt-4 border-t border-slate-200">
<button
onClick={() => closeWindow('sync')}
className="px-4 py-2 bg-white hover:bg-slate-100 text-ink-black border-[3px] border-ink-black font-bold text-xs shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none cursor-pointer"
>
CLOSE
</button>
<button
onClick={triggerSync}
disabled={syncing}
className="px-4 py-2 bg-[#ffcc00] hover:bg-yellow-300 text-ink-black border-[3px] border-ink-black font-bold text-xs shadow-[2px_2px_0px_0px_#1E293B] active:translate-y-0.5 active:shadow-none disabled:opacity-50 cursor-pointer"
>
{syncing ? 'SYNCING...' : 'SYNC NOW'}
</button>
</div>
</div>
</WindowShell>
{/* 5. My Computer Window (Interactive File Explorer) */}
<WindowShell
id="computer"
title="File Explorer"
icon="desktop_windows"
isOpen={windows.computer.isOpen}
isMinimized={windows.computer.isMinimized}
onClose={() => closeWindow('computer')}
onMinimize={() => minimizeWindow('computer')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="640px"
defaultHeight="480px"
titleBarColor="#006686"
isMobile={isMobile}
>
<FileExplorer onSetWallpaper={setWallpaperUrl} activeWallpaperUrl={wallpaperUrl} />
</WindowShell>
{/* 6. Daily Python Challenge Window */}
<WindowShell
id="python"
title="Python Challenge"
icon="sports_esports"
isOpen={windows.python.isOpen}
isMinimized={windows.python.isMinimized}
onClose={() => closeWindow('python')}
onMinimize={() => minimizeWindow('python')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="620px"
defaultHeight="580px"
titleBarColor="#9d4edd"
isMobile={isMobile}
>
<PythonChallenge />
</WindowShell>
{/* 7. MySQL Playground Window */}
<WindowShell
id="mysql"
title="MySQL Playground"
icon="database"
isOpen={windows.mysql.isOpen}
isMinimized={windows.mysql.isMinimized}
onClose={() => closeWindow('mysql')}
onMinimize={() => minimizeWindow('mysql')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="660px"
defaultHeight="580px"
titleBarColor="#0ea5e9"
isMobile={isMobile}
>
<MySQLPlayground />
</WindowShell>
{/* 8. API Control Panel Window */}
<WindowShell
id="settings"
title="Control Panel"
icon="key"
isOpen={windows.settings?.isOpen}
isMinimized={windows.settings?.isMinimized}
onClose={() => closeWindow('settings')}
onMinimize={() => minimizeWindow('settings')}
activeWindow={activeWindow}
setActiveWindow={setActiveWindow}
defaultWidth="500px"
defaultHeight="450px"
titleBarColor="#10b981"
isMobile={isMobile}
>
<Settings />
</WindowShell>
{/* Start Menu Popup */}
{isStartOpen && (
<div
onClick={(e) => e.stopPropagation()}
className="absolute bottom-14 left-0 w-64 bg-white border-[3px] border-ink-black shadow-[6px_6px_0px_0px_#1E293B] flex flex-col p-4 z-[60] ml-2 mb-2 select-none"
>
<div className="flex flex-col gap-2.5 font-window-title text-base font-bold">
<button
onClick={() => openWindow('queue')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>view_list</span>
Daily Queue
</button>
<button
onClick={() => openWindow('journey')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>route</span>
Journey Path
</button>
<button
onClick={triggerSync}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>cloud_sync</span>
Drive Sync
</button>
<button
onClick={() => openWindow('terminal')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>terminal</span>
Terminal
</button>
<button
onClick={() => openWindow('computer')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>desktop_windows</span>
My Computer
</button>
<button
onClick={() => openWindow('python')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>sports_esports</span>
Python Challenge
</button>
<button
onClick={() => openWindow('mysql')}
className="w-full text-left hover:bg-secondary-container px-3 py-1.5 border-2 border-transparent hover:border-ink-black transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg" style={{ fontVariationSettings: "'FILL' 1" }}>database</span>
MySQL Playground
</button>
{localStorage.getItem('algospaced_sandbox_mode') === 'true' && (
<button
onClick={() => openWindow('settings')}
className="w-full text-left hover:bg-[#10b981]/25 px-3 py-1.5 border-2 border-transparent hover:border-[#10b981] transition-colors cursor-pointer flex items-center gap-2"
>
<span className="material-symbols-outlined text-lg text-[#10b981]" style={{ fontVariationSettings: "'FILL' 1" }}>key</span>
Gemini API Config
</button>
)}
</div>
<div className="mt-4 flex justify-end border-t-[3px] border-ink-black pt-4 select-none">
<button
onClick={handleSignOut}
className="bg-highlight-pink text-white font-window-title px-4 py-2.5 border-[3px] border-ink-black shadow-[3px_3px_0px_0px_#1E293B] active:translate-y-0.5 active:translate-x-0.5 active:shadow-none hover:bg-rose-600 transition-all cursor-pointer font-bold uppercase text-xs"
>
Shut Down
</button>
</div>
</div>
)}
{/* Taskbar Bottom (Taskbar fixed layout) */}
<div
onClick={() => setIsStartOpen(false)}
className="fixed bottom-0 left-0 w-full z-50 flex items-center justify-between h-14 border-t-[4px] border-ink-black bg-[#0ea5e9] px-0 select-none shadow-[0_-4px_0_0_rgba(30,41,59,0.1)]"
>
<div className="flex items-center h-full overflow-x-auto custom-scrollbar">
{/* Start button */}
<button
onClick={(e) => {
e.stopPropagation();
setIsStartOpen(!isStartOpen);
}}
className={`h-full ${isMobile ? 'px-3' : 'px-6'} flex items-center gap-2 border-r-[4px] border-ink-black bg-[#39ff14] text-ink-black hover:bg-[#28e007] active:bg-[#1fb304] transition-colors shadow-[inset_-3px_-3px_0px_rgba(0,0,0,0.2),inset_3px_3px_0px_rgba(255,255,255,0.5)] font-bold cursor-pointer`}
>
<span className="material-symbols-outlined text-2xl font-bold" style={{ fontVariationSettings: "'FILL' 1" }}>
sports_esports
</span>
<span className={`font-display-lg ${isMobile ? 'text-sm' : 'text-lg'} italic tracking-wide`}>Start</span>
</button>
{/* Windows tabs in taskbar */}
{/* Daily Queue Tab */}
{windows.queue.isOpen && (
<button
onClick={() => toggleWindowMinimize('queue')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'queue' && !windows.queue.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">view_list</span>
<span>{isMobile ? 'Queue' : 'Daily Queue'}</span>
</button>
)}
{/* Journey Path Tab */}
{windows.journey.isOpen && (
<button
onClick={() => toggleWindowMinimize('journey')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'journey' && !windows.journey.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">route</span>
<span>{isMobile ? 'Journey' : 'Journey Path'}</span>
</button>
)}
{/* Terminal Tab */}
{windows.terminal.isOpen && (
<button
onClick={() => toggleWindowMinimize('terminal')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'terminal' && !windows.terminal.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">terminal</span>
<span>{isMobile ? 'Terminal' : 'Terminal'}</span>
</button>
)}
{/* Google Drive Sync Tab */}
{windows.sync.isOpen && (
<button
onClick={() => toggleWindowMinimize('sync')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'sync' && !windows.sync.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">cloud_sync</span>
<span>{isMobile ? 'Sync' : 'Drive Sync'}</span>
</button>
)}
{/* My Computer Tab */}
{windows.computer.isOpen && (
<button
onClick={() => toggleWindowMinimize('computer')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'computer' && !windows.computer.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">desktop_windows</span>
<span>{isMobile ? 'Comp' : 'My Computer'}</span>
</button>
)}
{/* Python Challenge Tab */}
{windows.python.isOpen && (
<button
onClick={() => toggleWindowMinimize('python')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'python' && !windows.python.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">sports_esports</span>
<span>{isMobile ? 'Python' : 'Python Challenge'}</span>
</button>
)}
{/* MySQL Playground Tab */}
{windows.mysql.isOpen && (
<button
onClick={() => toggleWindowMinimize('mysql')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'mysql' && !windows.mysql.isMinimized
? 'bg-paper-white text-ink-black font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold">database</span>
<span>{isMobile ? 'SQL' : 'MySQL Playground'}</span>
</button>
)}
{/* API Settings Tab */}
{windows.settings?.isOpen && (
<button
onClick={() => toggleWindowMinimize('settings')}
className={`h-full ${isMobile ? 'px-2 text-[10px] gap-1' : 'px-4 text-xs gap-2'} border-r-[4px] border-ink-black font-bold font-window-title cursor-pointer transition-all shadow-[inset_0px_3px_0px_rgba(255,255,255,0.8)] ${
activeWindow === 'settings' && !windows.settings.isMinimized
? 'bg-paper-white text-[#10b981] font-extrabold translate-y-[2px]'
: 'bg-surface-variant text-trash-gray hover:bg-white/40'
}`}
>
<span className="material-symbols-outlined text-sm font-bold text-[#10b981]">key</span>
<span>{isMobile ? 'API' : 'API Config'}</span>
</button>
)}
</div>
{/* System tray (wifi, clock, volume) */}
<div className="flex items-center h-full border-l-[4px] border-ink-black bg-[#0284c7] text-white px-4 gap-4 shadow-[inset_3px_3px_0px_rgba(0,0,0,0.2)]">
<span className="material-symbols-outlined cursor-pointer select-none text-lg">volume_up</span>
<span className="material-symbols-outlined cursor-pointer select-none text-lg">network_wifi</span>
<div className="font-arcade text-[9px] font-bold flex flex-col items-center leading-none justify-center select-none pt-0.5">
<span>{time}</span>
</div>
</div>
</div>
{/* Context Menu (Right Click Options) */}
{contextMenu.visible && (
<div
style={{ top: contextMenu.y, left: contextMenu.x }}
className="absolute bg-paper-white border-[3px] border-ink-black shadow-[4px_4px_0px_0px_#1E293B] z-[999] py-1 w-48 font-headline-md text-xs font-bold text-ink-black select-none animate-fade-in"
>
<button
onClick={toggleFullscreen}
className="w-full text-left px-4 py-2.5 hover:bg-secondary-container border-b-[2px] border-dashed border-ink-black flex items-center gap-2 cursor-pointer font-bold select-none outline-none"
>
<span className="material-symbols-outlined text-sm">fullscreen</span>
{isFullscreen ? 'Exit Full Screen' : 'Go Full Screen'}
</button>
<button
onClick={() => {
closeContextMenu();
document.getElementById('wallpaper-input')?.click();
}}
className="w-full text-left px-4 py-2.5 hover:bg-secondary-container flex items-center gap-2 cursor-pointer font-bold select-none outline-none"
>
<span className="material-symbols-outlined text-sm">image</span>
Change Wallpaper
</button>
</div>
)}
{/* Hidden Wallpaper File Input Selector */}
<input
type="file"
id="wallpaper-input"
accept="image/*"
onChange={handleWallpaperChange}
className="hidden"
/>
</div>
);
}