Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |