iamfebin's picture
Configure AlgoSpaced for Hugging Face Spaces deployment
aaa634c
Raw
History Blame Contribute Delete
46.4 kB
import React, { useState, useEffect } from 'react';
import { supabase } from '../supabaseClient';
interface ProblemFile {
id: string;
name: string;
pattern: string;
difficulty: string;
reference_code: string;
description: string | null;
box_level: number | null;
next_review: string | null;
last_reviewed: string | null;
times_correct: number;
total_attempts: number;
}
interface FileExplorerProps {
onSetWallpaper: (url: string) => void;
activeWallpaperUrl: string;
}
const WALLPAPER_PRESETS = [
{
name: 'Retro Arcade Grid.img',
url: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAMJHVhhK8_G7F8sbhn8F7w4AZWXb1O_-HLKvrZ_fJlbUVMcVuxxEOO_LlOh8qIwtAn2KvzvCwzmtNLLEZ5uYkCEsx8ZWXJR609qgSMRSX8LBBeskk4VzVXnyzsgIKCedeV2PvGxJlUNnledcWKCXqG9egQi8dgTcA7C2z82QyM73KZ4s7ZRzZHNupuQt1ocfcMl9E_x1PWFRridl751LIpyGZgednS5CmVw2rZvFc_tbp2QTxgVHJ_59myEBmy6aajbO06AhkKmCvI',
desc: 'Default AlgoSpaced purple grid and pixel theme background.'
},
{
name: 'Classic Board Logo.img',
url: 'https://lh3.googleusercontent.com/aida-public/AB6AXuD2tUGA2d7QiNyajDAWYK183zhNTgtAZpOXK3E9wRvHGs-t6ykWt9uPScYe_fziWzQI0pREcY_ThI341WvGMusyYmnAkagfuwx6wubIs1ES68DO8CCNAlcHcb2zOUU4MJeYuhWDy1uYRqyGYjIaDUqfgNWk2vm4WzwRqoorn2dxtZ6QdJhEKbXjG8fG9chkkvr68qJf0fLUL6bIqBXZg2FJ30S7zS3oZ4IZsug-wLyRYuJ4Gu_86snNUo1whNrBdZm5OMREfc9sLbwJ',
desc: 'System info and arcade machine diagram wallpaper.'
},
{
name: 'Terminal Console Icon.img',
url: 'https://lh3.googleusercontent.com/aida-public/AB6AXuA2AvcQ2oKWP8AJ8yR5nF82LLDEH1FKySUK-npnqEjvJ6Cs9xp8joCwH7x43oTCAC5xhO2rQdYzka3B6u72FNbdxMnsv3WJLye3bKochu8AnnRlS3szfYSaj724sYsiZo4n8G-oWqvl7C0rOGaDaU2nRH1xXmW2PBi6L2NryTsUaTCVZQKTKBBNNEOig5kh7B52e-iIuj2PTzaSjF9ebp6gMuCnBtJT8Sxo5Qsmvfm-t2U5r_qQpRiYj--Ut41T7NTh-tHgs4S4kBvz',
desc: 'Dark retro CRT terminal screen layout.'
},
{
name: 'Synthwave Sunset Grid.img',
url: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?q=80&w=1000&auto=format&fit=crop',
desc: 'Glowing neon grids and vector mountains styling.'
},
{
name: 'Matrix Code Rain.img',
url: 'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=1000&auto=format&fit=crop',
desc: 'Falling green digital characters rain.'
}
];
export default function FileExplorer({ onSetWallpaper, activeWallpaperUrl }: FileExplorerProps) {
const [currentPath, setCurrentPath] = useState<string[]>(['C:']);
const [problems, setProblems] = useState<ProblemFile[]>([]);
const [loading, setLoading] = useState(true);
const [selectedItem, setSelectedItem] = useState<string | null>(null);
// Dynamic wallpapers list merging presets and custom wallpaper from localStorage
const wallpapersList = React.useMemo(() => {
const list = [...WALLPAPER_PRESETS];
const customUrl = localStorage.getItem('customWallpaperUrl');
const customName = localStorage.getItem('customWallpaperName') || 'Custom Wallpaper.img';
if (customUrl) {
const exists = list.some(p => p.url === customUrl);
if (!exists) {
list.push({
name: customName,
url: customUrl,
desc: 'User uploaded custom background wallpaper.'
});
}
}
return list;
}, [activeWallpaperUrl]);
// Search & Filter state
const [searchQuery, setSearchQuery] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('All');
const [patternFilter, setPatternFilter] = useState('All');
// Drag & Drop state
const [dragOver, setDragOver] = useState(false);
// New File Creation state
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [newFileName, setNewFileName] = useState('');
const [newFilePattern, setNewFilePattern] = useState('');
const [newFileDifficulty, setNewFileDifficulty] = useState('Medium');
const [newFileCode, setNewFileCode] = useState('');
const [newFileDescription, setNewFileDescription] = useState('');
const [createError, setCreateError] = useState('');
const [creating, setCreating] = useState(false);
// Previewer Modal state
const [previewFile, setPreviewFile] = useState<ProblemFile | null>(null);
const [modalTab, setModalTab] = useState<'description' | 'code' | 'properties'>('description');
const [isEditing, setIsEditing] = useState(false);
const [editedCode, setEditedCode] = useState('');
const [editedDescription, setEditedDescription] = useState('');
const [saving, setSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [copied, setCopied] = useState(false);
const fetchProblems = async () => {
setLoading(true);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/explorer/problems`, {
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const data = await res.json();
setProblems(data);
}
} catch (err) {
console.error('Failed to fetch explorer files:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchProblems();
}, []);
const navigateTo = (folder: string) => {
setCurrentPath(['C:', folder]);
setSelectedItem(null);
};
const navigateUp = () => {
if (currentPath.length > 1) {
setCurrentPath(['C:']);
setSelectedItem(null);
}
};
// ZIP Solutions Exporter via Backend stream
const handleExportZip = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/explorer/export`, {
headers: { 'Authorization': `Bearer ${session.access_token}` }
});
if (res.ok) {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'algospaced_solutions.zip';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
} catch (err) {
console.error('Failed to download solutions ZIP:', err);
}
};
// Handle Save Solution Code changes
const handleSaveSolution = async () => {
if (!previewFile) return;
setSaving(true);
setSaveSuccess(false);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/explorer/problems/${previewFile.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({
name: previewFile.name,
pattern: previewFile.pattern,
difficulty: previewFile.difficulty,
reference_code: editedCode,
description: editedDescription
})
});
if (res.ok) {
setSaveSuccess(true);
setIsEditing(false);
// Refresh local problems list
const updated = problems.map(p => p.id === previewFile.id ? { ...p, reference_code: editedCode, description: editedDescription } : p);
setProblems(updated);
setPreviewFile({ ...previewFile, reference_code: editedCode, description: editedDescription });
setTimeout(() => setSaveSuccess(false), 2000);
}
} catch (err) {
console.error('Failed to update solution code:', err);
} finally {
setSaving(false);
}
};
// Create New Problem
const handleCreateProblem = async (e: React.FormEvent) => {
e.preventDefault();
if (!newFileName.trim()) {
setCreateError('Problem name cannot be empty');
return;
}
setCreating(true);
setCreateError('');
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session) return;
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/explorer/problems/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`
},
body: JSON.stringify({
name: newFileName.trim(),
pattern: newFilePattern.trim() || 'General',
difficulty: newFileDifficulty,
reference_code: newFileCode,
description: newFileDescription
})
});
const data = await res.json();
if (res.ok && data.success) {
setIsCreateModalOpen(false);
setNewFileName('');
setNewFilePattern('');
setNewFileCode('');
setNewFileDescription('');
fetchProblems();
} else {
setCreateError(data.detail || 'Failed to create problem.');
}
} catch {
setCreateError('Connection timed out.');
} finally {
setCreating(false);
}
};
// Drag and Drop files parser
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
};
const handleDragLeave = () => {
setDragOver(false);
};
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.name.endsWith('.py')) {
try {
const text = await file.text();
const guessedName = file.name
.replace('.py', '')
.replace(/_/g, ' ')
.replace(/-/g, ' ');
// Parse metadata tags if they exist inside script comments
let pattern = 'General';
let difficulty = 'Medium';
const patternMatch = text.match(/#\s*Pattern:\s*(.+)/i);
if (patternMatch) pattern = patternMatch[1].trim();
const difficultyMatch = text.match(/#\s*Difficulty:\s*(.+)/i);
if (difficultyMatch) {
const val = difficultyMatch[1].trim();
if (['Easy', 'Medium', 'Hard'].includes(val)) difficulty = val;
}
// Parse description: take all leading comment lines (excluding structural tags)
const lines = text.split('\n');
let descLines = [];
for (let line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('#')) {
if (trimmed.match(/#\s*(Pattern|Difficulty|Problem|Description):/i)) continue;
descLines.push(trimmed.replace(/^#\s*/, ''));
} else if (trimmed !== '') {
break;
}
}
const description = descLines.join('\n').trim();
// Load into New File Creator modal
setNewFileName(guessedName);
setNewFilePattern(pattern);
setNewFileDifficulty(difficulty);
setNewFileCode(text);
setNewFileDescription(description);
setIsCreateModalOpen(true);
} catch (err) {
console.error('Failed to parse uploaded script:', err);
}
}
}
};
const handleCopyCode = (text: string) => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const activeFolder = currentPath.length > 1 ? currentPath[1] : 'root';
// Statistics calculation for the visualizer
const totalProblems = problems.length;
const completedSolutions = problems.filter(p => p.reference_code && p.reference_code.trim()).length;
const completionPercent = totalProblems > 0 ? Math.round((completedSolutions / totalProblems) * 100) : 0;
const boxCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
problems.forEach(p => {
if (p.box_level !== null && p.box_level !== undefined) {
const lvl = p.box_level as 1|2|3|4|5;
if (lvl >= 1 && lvl <= 5) boxCounts[lvl]++;
}
});
// Extract unique patterns to populate filter dropdown dynamically
const uniquePatterns = Array.from(new Set(problems.map(p => p.pattern).filter(Boolean)));
// Filter solutions list
const filteredProblems = problems.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase());
const matchesDifficulty = difficultyFilter === 'All' || p.difficulty === difficultyFilter;
const matchesPattern = patternFilter === 'All' || p.pattern === patternFilter;
return matchesSearch && matchesDifficulty && matchesPattern;
});
return (
<div className="w-full h-full bg-paper-white text-ink-black flex flex-col overflow-hidden text-xs md:text-sm font-sans select-none">
{/* 1. Address Bar & Navigation */}
<div className="bg-slate-200 border-b-[3px] border-ink-black p-2 flex items-center justify-between shrink-0">
<div className="flex items-center gap-2 flex-1">
<button
onClick={navigateUp}
disabled={currentPath.length === 1}
className="w-7 h-7 bg-white border-2 border-ink-black flex items-center justify-center hover:bg-slate-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer active:translate-y-0.5"
title="Up One Folder"
>
<span className="material-symbols-outlined text-[16px] font-bold">arrow_upward</span>
</button>
<div className="bg-white border-2 border-ink-black px-3 py-1 font-mono text-[10px] flex-1 max-w-[280px] truncate">
{currentPath.join('\\')}
</div>
</div>
{/* Action Link bar */}
<div className="flex gap-3 text-[10px] font-bold pr-2 font-mono items-center">
{activeFolder === 'Database_Codes' && (
<>
<button
onClick={() => setIsCreateModalOpen(true)}
className="text-blue-700 hover:underline cursor-pointer flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-[12px] font-bold">add_box</span>
NEW FILE
</button>
<span className="text-slate-400">|</span>
<button
onClick={handleExportZip}
className="text-purple-700 hover:underline cursor-pointer flex items-center gap-0.5"
>
<span className="material-symbols-outlined text-[12px] font-bold">downloadzip</span>
BACKUP (.ZIP)
</button>
<span className="text-slate-400">|</span>
</>
)}
<span className="text-slate-500">
{activeFolder === 'root' ? '2 Folder(s)' : activeFolder === 'Database_Codes' ? `${filteredProblems.length} of ${problems.length} item(s)` : `${wallpapersList.length} image(s)`}
</span>
</div>
</div>
{/* 2. Main Workspace Layout */}
<div className="flex-1 flex overflow-hidden">
{/* Left Side: Navigation Sidebar & Disk Stats Visualizer */}
<aside className="w-1/3 border-r-[3px] border-ink-black bg-[#fdf8e1] p-3 overflow-y-auto custom-scrollbar flex flex-col gap-4 shrink-0">
{/* Folders tree */}
<div>
<span className="text-[9px] font-arcade text-slate-500 block uppercase mb-1.5">Folders</span>
<div className="space-y-1">
<button
onClick={() => setCurrentPath(['C:'])}
className={`w-full text-left p-1.5 flex items-center gap-1.5 font-semibold border-2 border-transparent hover:bg-slate-100 rounded ${currentPath.length === 1 ? 'bg-white border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-[#855400] text-sm" style={{ fontVariationSettings: "'FILL' 1" }}>desktop_windows</span>
<span>C: (Root)</span>
</button>
<div className="pl-4 space-y-1">
<button
onClick={() => navigateTo('Database_Codes')}
className={`w-full text-left p-1.5 flex items-center gap-1.5 font-semibold border-2 border-transparent hover:bg-slate-100 rounded ${activeFolder === 'Database_Codes' ? 'bg-white border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-yellow-600 text-sm" style={{ fontVariationSettings: "'FILL' 1" }}>folder</span>
<span className="truncate">Database_Codes</span>
</button>
<button
onClick={() => navigateTo('Wallpapers')}
className={`w-full text-left p-1.5 flex items-center gap-1.5 font-semibold border-2 border-transparent hover:bg-slate-100 rounded ${activeFolder === 'Wallpapers' ? 'bg-white border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-yellow-600 text-sm" style={{ fontVariationSettings: "'FILL' 1" }}>folder</span>
<span className="truncate">Wallpapers</span>
</button>
</div>
</div>
</div>
{/* Feature 5: Disk Stats Visualizer */}
<div className="border-t-2 border-dashed border-slate-300 pt-3">
<span className="text-[9px] font-arcade text-slate-500 block uppercase mb-2">System Statistics</span>
<div className="space-y-3 font-sans text-[11px] font-bold text-slate-700">
{/* Completion Rate ProgressBar */}
<div className="space-y-1">
<div className="flex justify-between text-[10px]">
<span>CODE COMPLETION:</span>
<span className="font-mono text-emerald-600">{completionPercent}%</span>
</div>
<div className="w-full h-4 bg-white border-2 border-ink-black overflow-hidden relative">
<div
style={{ width: `${completionPercent}%` }}
className="h-full bg-emerald-500 border-r-2 border-ink-black transition-all duration-500"
/>
</div>
</div>
{/* Box Level Counts visual bar charts */}
<div className="space-y-1.5 mt-2">
<span className="text-[9px] uppercase text-slate-500 block">Leitner Distribution:</span>
{([1, 2, 3, 4, 5] as const).map(boxNum => {
const count = boxCounts[boxNum];
const maxCount = Math.max(...Object.values(boxCounts), 1);
const barWidth = Math.max(8, Math.round((count / maxCount) * 100));
return (
<div key={boxNum} className="flex items-center gap-2">
<span className="w-9 font-mono text-[10px] text-slate-400">Box {boxNum}:</span>
<div className="flex-1 h-3 bg-white border border-ink-black overflow-hidden relative">
<div
style={{ width: `${barWidth}%` }}
className="h-full bg-[#0ea5e9] border-r border-ink-black transition-all"
/>
</div>
<span className="w-4 text-right font-mono text-[10px] text-slate-600">{count}</span>
</div>
);
})}
</div>
</div>
</div>
</aside>
{/* Right Side: Folder View Contents & Upload Area */}
<main
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`flex-1 bg-white p-4 overflow-y-auto custom-scrollbar flex flex-col relative transition-all ${
dragOver ? 'bg-blue-50 border-4 border-dashed border-blue-500 m-2' : ''
}`}
>
{dragOver && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-blue-800 bg-white/80 pointer-events-none select-none z-[10] gap-2 font-sans font-bold">
<span className="material-symbols-outlined text-5xl animate-bounce">upload_file</span>
<div className="text-sm">DROP SOLUTIONS PYTHON SCRIPT (.PY) HERE</div>
<p className="text-[10px] text-slate-400">Will load and parse file metadata for import</p>
</div>
)}
{loading ? (
<div className="flex-1 flex flex-col items-center justify-center font-arcade text-[8px] animate-pulse">
READING SYSTEM DISK...
</div>
) : (
<>
{/* Feature 2: Search and Filter Bar Header */}
{activeFolder === 'Database_Codes' && (
<div className="mb-4 bg-slate-50 border-[2px] border-ink-black p-2 flex flex-col gap-2 shrink-0 select-none">
<div className="flex gap-2">
{/* Search Field */}
<div className="flex-1 bg-white border border-slate-300 rounded px-2 py-1 flex items-center gap-1">
<span className="material-symbols-outlined text-slate-400 text-sm">search</span>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent outline-none border-none p-0 text-[11px] focus:ring-0 font-sans"
placeholder="Search codes by name..."
/>
</div>
</div>
<div className="flex gap-3 text-[10px] font-bold text-slate-600">
{/* Difficulty Filter */}
<div className="flex items-center gap-1">
<span>Diff:</span>
<select
value={difficultyFilter}
onChange={(e) => setDifficultyFilter(e.target.value)}
className="bg-white border border-slate-300 p-0.5 text-[9px] rounded font-sans cursor-pointer focus:ring-1 outline-none"
>
<option value="All">All</option>
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
</select>
</div>
{/* Pattern Filter */}
<div className="flex items-center gap-1 flex-1">
<span>Pattern:</span>
<select
value={patternFilter}
onChange={(e) => setPatternFilter(e.target.value)}
className="bg-white border border-slate-300 p-0.5 text-[9px] rounded font-sans cursor-pointer max-w-[120px] focus:ring-1 outline-none truncate"
>
<option value="All">All</option>
{uniquePatterns.map(pat => (
<option key={pat} value={pat}>{pat}</option>
))}
</select>
</div>
</div>
</div>
)}
{/* Root Content View */}
{activeFolder === 'root' && (
<div className="grid grid-cols-2 gap-4">
<div
onDoubleClick={() => navigateTo('Database_Codes')}
onClick={() => setSelectedItem('codes')}
className={`flex flex-col items-center justify-center p-3 border-[3px] border-transparent hover:bg-slate-50 cursor-pointer rounded select-none ${selectedItem === 'codes' ? 'bg-yellow-50 border-dashed border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-5xl text-yellow-500" style={{ fontVariationSettings: "'FILL' 1" }}>folder</span>
<span className="font-bold text-center mt-2 text-xs">Database_Codes</span>
<span className="text-[9px] text-slate-400 mt-0.5">Synced solution files</span>
</div>
<div
onDoubleClick={() => navigateTo('Wallpapers')}
onClick={() => setSelectedItem('wallpapers')}
className={`flex flex-col items-center justify-center p-3 border-[3px] border-transparent hover:bg-slate-50 cursor-pointer rounded select-none ${selectedItem === 'wallpapers' ? 'bg-yellow-50 border-dashed border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-5xl text-yellow-500" style={{ fontVariationSettings: "'FILL' 1" }}>folder</span>
<span className="font-bold text-center mt-2 text-xs">Wallpapers</span>
<span className="text-[9px] text-slate-400 mt-0.5">Desktop wallpaper images</span>
</div>
</div>
)}
{/* Database Codes Content View */}
{activeFolder === 'Database_Codes' && (
filteredProblems.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center text-center text-slate-400 p-6 italic select-none">
<span className="material-symbols-outlined text-3xl mb-1">drafts</span>
No matching files found. Drag and drop a .py script to upload/create!
</div>
) : (
<div className="grid grid-cols-3 gap-2">
{filteredProblems.map((prob) => {
const cleanName = prob.name.replace(/\s+/g, '_');
const fileName = `${cleanName}.py`;
const isSelected = selectedItem === prob.id;
return (
<div
key={prob.id}
onClick={() => setSelectedItem(prob.id)}
onDoubleClick={() => {
setPreviewFile(prob);
setEditedCode(prob.reference_code || '');
setEditedDescription(prob.description || '');
setModalTab('description');
setIsEditing(false);
}}
className={`flex flex-col items-center p-2 border-[3px] border-transparent hover:bg-slate-50 cursor-pointer rounded select-none text-center ${isSelected ? 'bg-yellow-50 border-dashed border-ink-black' : ''}`}
>
<span className="material-symbols-outlined text-3xl text-emerald-600">article</span>
<span className="font-bold text-[10px] truncate w-full mt-1.5 leading-tight">{fileName}</span>
<span className="text-[8px] text-slate-400 font-bold uppercase mt-0.5">{prob.difficulty}</span>
</div>
);
})}
</div>
)
)}
{/* Wallpapers Content View */}
{activeFolder === 'Wallpapers' && (
<div className="grid grid-cols-2 gap-3 select-none">
{wallpapersList.map((paper) => {
const isSelected = selectedItem === paper.name;
const isActive = activeWallpaperUrl === paper.url;
return (
<div
key={paper.name}
onClick={() => setSelectedItem(paper.name)}
className={`border-[3px] p-2 flex flex-col cursor-pointer bg-slate-50 relative hover:bg-slate-100 ${isSelected ? 'border-ink-black bg-yellow-50 shadow-[2px_2px_0_0_#1E293B]' : 'border-slate-300'}`}
>
<div className="aspect-video w-full border border-slate-300 overflow-hidden bg-black shrink-0 relative">
<img src={paper.url} alt={paper.name} className="w-full h-full object-cover select-none" />
{isActive && (
<div className="absolute top-1 right-1 bg-green-500 text-white rounded-full w-5 h-5 flex items-center justify-center border border-white" title="Active Wallpaper">
<span className="material-symbols-outlined text-[12px] font-extrabold">check</span>
</div>
)}
</div>
<span className="font-bold text-[10px] mt-1.5 truncate">{paper.name}</span>
<p className="text-[8px] text-slate-400 font-sans leading-tight mt-0.5 line-clamp-2">{paper.desc}</p>
{isSelected && (
<button
onClick={(e) => {
e.stopPropagation();
onSetWallpaper(paper.url);
localStorage.setItem('desktopWallpaperUrl', paper.url);
}}
className="mt-2 w-full py-1 bg-highlight-pink text-white border-2 border-ink-black font-window-title font-bold text-[9px] hover:bg-pink-400 shadow-[1px_1px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none transition-all text-center cursor-pointer"
>
APPLY BACKGROUND
</button>
)}
</div>
);
})}
</div>
)}
</>
)}
</main>
</div>
{/* Feature 3 & 1: Two-Tab File Previewer & Editor Inspector Modal */}
{previewFile && (
<div className="absolute inset-0 bg-black/60 z-[999] flex items-center justify-center p-4">
<div className="w-[90%] h-[90%] bg-paper-white border-[4px] border-ink-black shadow-[6px_6px_0px_0px_#1E293B] flex flex-col overflow-hidden animate-scale-up text-ink-black">
{/* Modal Title bar */}
<div className="bg-emerald-600 text-white p-2 border-b-[3px] border-ink-black flex justify-between items-center shrink-0 select-none">
<div className="flex items-center gap-1.5">
<span className="material-symbols-outlined text-sm">article</span>
<span className="font-window-title font-bold text-xs">{previewFile.name.replace(/\s+/g, '_')}.py</span>
</div>
<button
onClick={() => setPreviewFile(null)}
className="bg-white text-ink-black border-[2px] border-ink-black w-5 h-5 flex items-center justify-center font-bold hover:bg-red-500 hover:text-white cursor-pointer"
>
X
</button>
</div>
{/* Triple Tab selectors */}
<div className="bg-slate-200 border-b-2 border-ink-black flex shrink-0 select-none">
<button
onClick={() => setModalTab('description')}
className={`px-4 py-2 border-r-2 border-ink-black font-bold text-[11px] cursor-pointer ${modalTab === 'description' ? 'bg-white translate-y-[2px]' : 'bg-slate-300 hover:bg-slate-100'}`}
>
PROBLEM DESCRIPTION
</button>
<button
onClick={() => setModalTab('code')}
className={`px-4 py-2 border-r-2 border-ink-black font-bold text-[11px] cursor-pointer ${modalTab === 'code' ? 'bg-white translate-y-[2px]' : 'bg-slate-300 hover:bg-slate-100'}`}
>
CODE VIEW & EDIT
</button>
<button
onClick={() => setModalTab('properties')}
className={`px-4 py-2 border-r-2 border-ink-black font-bold text-[11px] cursor-pointer ${modalTab === 'properties' ? 'bg-white translate-y-[2px]' : 'bg-slate-300 hover:bg-slate-100'}`}
>
LEITNER PROPERTIES
</button>
</div>
{/* TAB CONTENT: Problem Description */}
{modalTab === 'description' && (
<div className="flex-1 flex flex-col overflow-hidden bg-slate-900 relative">
{isEditing ? (
<textarea
value={editedDescription}
onChange={(e) => setEditedDescription(e.target.value)}
className="flex-1 w-full bg-slate-950 text-[#4ADE80] font-mono p-4 outline-none border-none text-xs leading-relaxed resize-none custom-scrollbar"
placeholder="Enter problem description / question body here..."
spellCheck="false"
/>
) : (
<div className="flex-1 p-4 bg-slate-900 text-[#4ADE80] font-sans text-xs leading-relaxed overflow-y-auto custom-scrollbar select-text">
<div className="whitespace-pre-wrap font-semibold">{previewFile.description || 'No description provided for this problem.'}</div>
</div>
)}
{/* Edit indicators */}
{isEditing && (
<div className="absolute top-2 right-2 bg-yellow-400 text-ink-black border border-ink-black px-2 py-0.5 text-[9px] font-bold select-none animate-pulse">
EDIT MODE ACTIVE
</div>
)}
</div>
)}
{/* TAB CONTENT: Code preview & editing */}
{modalTab === 'code' && (
<div className="flex-1 flex flex-col overflow-hidden bg-slate-900 relative">
{isEditing ? (
<textarea
value={editedCode}
onChange={(e) => setEditedCode(e.target.value)}
className="flex-1 w-full bg-slate-950 text-[#4ADE80] font-mono p-4 outline-none border-none text-xs leading-relaxed resize-none custom-scrollbar"
spellCheck="false"
/>
) : (
<textarea
readOnly
value={previewFile.reference_code || '# Write your solution here.'}
className="flex-1 w-full bg-slate-900 text-[#4ADE80] font-mono p-4 outline-none border-none text-xs leading-relaxed resize-none custom-scrollbar"
spellCheck="false"
/>
)}
{/* Edit indicators */}
{isEditing && (
<div className="absolute top-2 right-2 bg-yellow-400 text-ink-black border border-ink-black px-2 py-0.5 text-[9px] font-bold select-none animate-pulse">
EDIT MODE ACTIVE
</div>
)}
</div>
)}
{/* TAB CONTENT: Leitner properties */}
{modalTab === 'properties' && (
<div className="flex-1 p-6 overflow-y-auto custom-scrollbar bg-slate-50 font-sans leading-relaxed select-text flex flex-col gap-4">
<h3 className="font-arcade text-[10px] border-b border-slate-300 pb-1 text-slate-500">Card Diagnostics</h3>
<div className="grid grid-cols-2 gap-4">
<div className="border-[3px] border-ink-black bg-white p-3 space-y-2">
<span className="text-[9px] font-arcade text-[#0ea5e9] block">Scheduling Status</span>
<div>Leitner Combo Box: <span className="font-mono font-bold text-lg text-sky-600">{previewFile.box_level || 'N/A (Box 1)'}</span></div>
<div>Next Review Date: <span className="font-mono text-xs font-semibold">{previewFile.next_review ? new Date(previewFile.next_review).toLocaleString() : 'N/A (Due)'}</span></div>
<div>Last Reviewed: <span className="font-mono text-xs font-semibold">{previewFile.last_reviewed ? new Date(previewFile.last_reviewed).toLocaleDateString() : 'Never'}</span></div>
</div>
<div className="border-[3px] border-ink-black bg-white p-3 space-y-2">
<span className="text-[9px] font-arcade text-emerald-600 block">Performance History</span>
<div>Total Attempts: <span className="font-mono font-bold text-sm">{previewFile.total_attempts}</span></div>
<div>Times Correct: <span className="font-mono font-bold text-sm text-green-600">{previewFile.times_correct}</span></div>
<div>Success Ratio: <span className="font-mono font-bold text-sm text-purple-700">
{previewFile.total_attempts > 0 ? `${Math.round((previewFile.times_correct / previewFile.total_attempts) * 100)}%` : '0%'}
</span></div>
</div>
</div>
<div className="border-[3px] border-ink-black bg-white p-3 space-y-1.5 text-xs font-semibold">
<span className="text-[9px] font-arcade text-slate-400 block">Properties</span>
<div>File ID: <span className="font-mono text-[10px] text-slate-500">{previewFile.id}</span></div>
<div>Logical Pattern: <span className="text-blue-600">{previewFile.pattern}</span></div>
<div>Difficulty Tag: <span className="uppercase">{previewFile.difficulty}</span></div>
</div>
</div>
)}
{/* Modal Footer controls */}
<div className="p-3 bg-slate-100 border-t-[3px] border-ink-black flex justify-between items-center shrink-0 select-none">
<div>
{saveSuccess && (
<span className="text-green-600 font-bold text-xs animate-pulse">✓ Solution saved!</span>
)}
</div>
<div className="flex gap-2">
{(modalTab === 'code' || modalTab === 'description') && (
isEditing ? (
<>
<button
onClick={() => {
setEditedCode(previewFile.reference_code || '');
setEditedDescription(previewFile.description || '');
setIsEditing(false);
}}
className="px-3 py-1.5 bg-white hover:bg-slate-50 text-ink-black border-2 border-ink-black font-bold text-xs cursor-pointer"
>
CANCEL
</button>
<button
disabled={saving}
onClick={handleSaveSolution}
className="px-3 py-1.5 bg-[#4ADE80] hover:bg-emerald-400 text-ink-black border-2 border-ink-black font-bold text-xs cursor-pointer shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none"
>
{saving ? 'SAVING...' : 'SAVE CHANGES'}
</button>
</>
) : (
<button
onClick={() => setIsEditing(true)}
className="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-300 text-ink-black border-2 border-ink-black font-bold text-xs cursor-pointer shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none flex items-center gap-1"
>
<span className="material-symbols-outlined text-sm">edit</span>
EDIT FILE
</button>
)
)}
<button
onClick={() => handleCopyCode(isEditing ? editedCode : previewFile.reference_code)}
className="px-3 py-1.5 bg-[#ffcc00] hover:bg-yellow-300 text-ink-black border-2 border-ink-black font-bold text-xs shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none cursor-pointer flex items-center gap-1"
>
<span className="material-symbols-outlined text-sm">content_copy</span>
{copied ? 'COPIED!' : 'COPY CODE'}
</button>
<button
onClick={() => setPreviewFile(null)}
className="px-3 py-1.5 bg-white hover:bg-slate-50 text-ink-black border-2 border-ink-black font-bold text-xs shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none cursor-pointer"
>
CLOSE
</button>
</div>
</div>
</div>
</div>
)}
{/* Feature 1: New File Creator Modal Dialog */}
{isCreateModalOpen && (
<div className="absolute inset-0 bg-black/60 z-[999] flex items-center justify-center p-4 select-none">
<div className="w-[80%] max-w-[500px] bg-paper-white border-[4px] border-ink-black shadow-[6px_6px_0px_0px_#1E293B] flex flex-col overflow-hidden animate-scale-up text-ink-black font-sans">
{/* Modal Header */}
<div className="bg-primary-purple text-white p-2.5 border-b-[3px] border-ink-black flex justify-between items-center font-bold">
<span className="font-window-title text-xs">CREATE NEW SOLUTION FILE</span>
<button
onClick={() => setIsCreateModalOpen(false)}
className="bg-white text-ink-black border-[2px] border-ink-black w-5 h-5 flex items-center justify-center font-bold hover:bg-red-500 hover:text-white cursor-pointer"
>
X
</button>
</div>
{/* Modal Body form */}
<form onSubmit={handleCreateProblem} className="p-4 space-y-3 flex-1 overflow-y-auto custom-scrollbar">
{createError && (
<div className="p-2 border-2 border-red-500 bg-red-50 text-red-800 text-[10px] font-mono leading-relaxed">
{createError}
</div>
)}
{/* Problem Name */}
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Problem Name:</label>
<input
type="text"
required
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="e.g. Two Sum"
className="bg-white border-2 border-ink-black p-1.5 text-xs outline-none focus:ring-1 focus:ring-primary-purple font-semibold"
/>
</div>
{/* Logical Pattern Category */}
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Pattern Category:</label>
<input
type="text"
value={newFilePattern}
onChange={(e) => setNewFilePattern(e.target.value)}
placeholder="e.g. Sliding Window"
className="bg-white border-2 border-ink-black p-1.5 text-xs outline-none focus:ring-1 focus:ring-primary-purple font-semibold"
/>
</div>
{/* Difficulty Selection */}
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Difficulty:</label>
<select
value={newFileDifficulty}
onChange={(e) => setNewFileDifficulty(e.target.value)}
className="bg-white border-2 border-ink-black p-1.5 text-xs outline-none focus:ring-1 focus:ring-primary-purple font-bold cursor-pointer"
>
<option value="Easy">Easy</option>
<option value="Medium">Medium</option>
<option value="Hard">Hard</option>
</select>
</div>
{/* Problem Description */}
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Problem Description:</label>
<textarea
value={newFileDescription}
onChange={(e) => setNewFileDescription(e.target.value)}
placeholder="e.g. Given an array of integers, return indices of the two numbers such that they add up to a specific target."
className="bg-white border-2 border-ink-black p-1.5 text-xs outline-none focus:ring-1 focus:ring-primary-purple font-semibold h-[70px] resize-none"
/>
</div>
{/* Solution Code */}
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-slate-500 uppercase">Reference Python Code:</label>
<textarea
value={newFileCode}
onChange={(e) => setNewFileCode(e.target.value)}
placeholder="def solve():\n pass"
className="bg-slate-900 text-[#4ADE80] font-mono p-2 border-2 border-ink-black rounded outline-none h-[120px] text-xs leading-relaxed resize-none"
/>
</div>
{/* Action Buttons */}
<div className="pt-3 border-t border-slate-200 flex justify-end gap-3 font-semibold">
<button
type="button"
onClick={() => setIsCreateModalOpen(false)}
className="px-3 py-1.5 bg-white hover:bg-slate-50 border-2 border-ink-black text-xs cursor-pointer"
>
CANCEL
</button>
<button
type="submit"
disabled={creating}
className="px-3 py-1.5 bg-primary-purple text-white border-2 border-ink-black text-xs cursor-pointer shadow-[2px_2px_0px_rgba(0,0,0,1)] active:translate-y-0.5 active:shadow-none"
>
{creating ? 'CREATING...' : 'ADD SOLUTION'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}