Spaces:
Running
Running
made some changes with respect to quiz and flutter MCP
Browse files- app/components/Calendar.tsx +71 -26
- app/components/FlutterRunner.tsx +155 -38
- app/components/LaTeXEditor.tsx +148 -37
- app/components/QuizApp.tsx +56 -40
app/components/Calendar.tsx
CHANGED
|
@@ -30,6 +30,7 @@ interface CalendarEvent extends Holiday {
|
|
| 30 |
type ViewMode = 'day' | 'week' | 'month' | 'year'
|
| 31 |
|
| 32 |
export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: CalendarProps) {
|
|
|
|
| 33 |
const [currentDate, setCurrentDate] = useState(new Date())
|
| 34 |
const [view, setView] = useState<ViewMode>('month')
|
| 35 |
const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
|
|
@@ -38,6 +39,25 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 38 |
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
| 39 |
const [sidebarOpen, setSidebarOpen] = useState(true)
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
// Scroll to 8 AM on mount for day/week views
|
| 42 |
useEffect(() => {
|
| 43 |
if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
|
|
@@ -184,8 +204,8 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 184 |
onMaximize={onMaximize}
|
| 185 |
onFocus={onFocus}
|
| 186 |
zIndex={zIndex}
|
| 187 |
-
width={
|
| 188 |
-
height={
|
| 189 |
x={50}
|
| 190 |
y={50}
|
| 191 |
className="calendar-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
|
|
@@ -253,28 +273,34 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 253 |
{/* Main Content */}
|
| 254 |
<div className="flex-1 flex flex-col bg-transparent">
|
| 255 |
{/* Toolbar */}
|
| 256 |
-
<div className="h-14 flex items-center justify-between px-6 border-b border-white/10 bg-[#252525]/30 backdrop-blur-sm">
|
| 257 |
-
<div className="flex items-center gap-4">
|
| 258 |
-
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-2 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white">
|
| 259 |
-
<List size={
|
| 260 |
</button>
|
| 261 |
-
<div className="text-2xl font-light tracking-tight">{renderHeaderTitle()}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
</div>
|
| 263 |
|
| 264 |
-
<div className="flex items-center gap-4">
|
| 265 |
-
<div className="flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
|
| 266 |
-
<button onClick={handlePrev} className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
|
| 267 |
-
<CaretLeft size={
|
| 268 |
</button>
|
| 269 |
-
<button onClick={handleToday} className="px-3 py-1 text-sm font-medium text-gray-300 hover:text-white transition-colors">
|
| 270 |
Today
|
| 271 |
</button>
|
| 272 |
-
<button onClick={handleNext} className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
|
| 273 |
-
<CaretRight size={
|
| 274 |
</button>
|
| 275 |
</div>
|
| 276 |
|
| 277 |
-
<div className="flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
|
| 278 |
{(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
|
| 279 |
<button
|
| 280 |
key={v}
|
|
@@ -289,10 +315,22 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 289 |
))}
|
| 290 |
</div>
|
| 291 |
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
<Plus size={20} />
|
| 294 |
</button>
|
| 295 |
-
<button className="p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
|
| 296 |
<MagnifyingGlass size={20} />
|
| 297 |
</button>
|
| 298 |
</div>
|
|
@@ -310,11 +348,12 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 310 |
transition={{ duration: 0.2 }}
|
| 311 |
className="h-full flex flex-col overflow-auto [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2"
|
| 312 |
>
|
| 313 |
-
<div className="
|
| 314 |
<div className="grid grid-cols-7 border-b border-white/5 bg-black/10 sticky top-0 z-10 backdrop-blur-sm">
|
| 315 |
{weekDays.map(day => (
|
| 316 |
-
<div key={day} className="py-2 text-right pr-4 text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 317 |
-
{day}
|
|
|
|
| 318 |
</div>
|
| 319 |
))}
|
| 320 |
</div>
|
|
@@ -328,7 +367,7 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 328 |
<div
|
| 329 |
key={index}
|
| 330 |
className={`
|
| 331 |
-
border-b border-r border-white/5 p-1 relative group transition-colors
|
| 332 |
${!dateObj.isCurrentMonth ? 'bg-black/20 text-gray-600' : 'hover:bg-white/5'}
|
| 333 |
`}
|
| 334 |
onClick={() => {
|
|
@@ -336,10 +375,10 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 336 |
setView('day')
|
| 337 |
}}
|
| 338 |
>
|
| 339 |
-
<div className="flex justify-end mb-1">
|
| 340 |
<span
|
| 341 |
className={`
|
| 342 |
-
text-sm font-medium w-7 h-7 flex items-center justify-center rounded-full
|
| 343 |
${isCurrentDay
|
| 344 |
? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
|
| 345 |
: dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
|
|
@@ -348,18 +387,24 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
|
|
| 348 |
{dateObj.day}
|
| 349 |
</span>
|
| 350 |
</div>
|
| 351 |
-
<div className="space-y-
|
| 352 |
-
{dayEvents.map((event, i) => (
|
| 353 |
<div
|
| 354 |
key={i}
|
| 355 |
className={`
|
| 356 |
-
text-[
|
| 357 |
${event.color || 'bg-blue-500'} text-white font-medium shadow-sm backdrop-blur-sm bg-opacity-80
|
| 358 |
`}
|
|
|
|
| 359 |
>
|
| 360 |
{event.title}
|
| 361 |
</div>
|
| 362 |
))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
</div>
|
| 364 |
</div>
|
| 365 |
)
|
|
|
|
| 30 |
type ViewMode = 'day' | 'week' | 'month' | 'year'
|
| 31 |
|
| 32 |
export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: CalendarProps) {
|
| 33 |
+
const [windowSize, setWindowSize] = useState({ width: 1100, height: 750 })
|
| 34 |
const [currentDate, setCurrentDate] = useState(new Date())
|
| 35 |
const [view, setView] = useState<ViewMode>('month')
|
| 36 |
const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
|
|
|
|
| 39 |
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
| 40 |
const [sidebarOpen, setSidebarOpen] = useState(true)
|
| 41 |
|
| 42 |
+
// Handle window resize
|
| 43 |
+
useEffect(() => {
|
| 44 |
+
const handleResize = () => {
|
| 45 |
+
const isMobile = window.innerWidth < 768
|
| 46 |
+
setWindowSize({
|
| 47 |
+
width: Math.min(1100, window.innerWidth - (isMobile ? 20 : 40)),
|
| 48 |
+
height: Math.min(750, window.innerHeight - (isMobile ? 20 : 40))
|
| 49 |
+
})
|
| 50 |
+
// Auto-close sidebar on mobile
|
| 51 |
+
if (isMobile && sidebarOpen) {
|
| 52 |
+
setSidebarOpen(false)
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
handleResize() // Set initial size
|
| 57 |
+
window.addEventListener('resize', handleResize)
|
| 58 |
+
return () => window.removeEventListener('resize', handleResize)
|
| 59 |
+
}, [sidebarOpen])
|
| 60 |
+
|
| 61 |
// Scroll to 8 AM on mount for day/week views
|
| 62 |
useEffect(() => {
|
| 63 |
if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
|
|
|
|
| 204 |
onMaximize={onMaximize}
|
| 205 |
onFocus={onFocus}
|
| 206 |
zIndex={zIndex}
|
| 207 |
+
width={windowSize.width}
|
| 208 |
+
height={windowSize.height}
|
| 209 |
x={50}
|
| 210 |
y={50}
|
| 211 |
className="calendar-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
|
|
|
|
| 273 |
{/* Main Content */}
|
| 274 |
<div className="flex-1 flex flex-col bg-transparent">
|
| 275 |
{/* Toolbar */}
|
| 276 |
+
<div className="h-12 sm:h-14 flex items-center justify-between px-2 sm:px-6 border-b border-white/10 bg-[#252525]/30 backdrop-blur-sm">
|
| 277 |
+
<div className="flex items-center gap-2 sm:gap-4">
|
| 278 |
+
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-1.5 sm:p-2 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white">
|
| 279 |
+
<List size={18} className="sm:w-5 sm:h-5" />
|
| 280 |
</button>
|
| 281 |
+
<div className="text-base sm:text-2xl font-light tracking-tight hidden sm:block">{renderHeaderTitle()}</div>
|
| 282 |
+
<div className="text-sm font-medium tracking-tight sm:hidden">
|
| 283 |
+
{view === 'month' && `${monthNames[currentDate.getMonth()].substr(0, 3)} ${currentDate.getFullYear()}`}
|
| 284 |
+
{view === 'year' && currentDate.getFullYear()}
|
| 285 |
+
{view === 'day' && `${currentDate.getDate()} ${monthNames[currentDate.getMonth()].substr(0, 3)}`}
|
| 286 |
+
{view === 'week' && 'Week View'}
|
| 287 |
+
</div>
|
| 288 |
</div>
|
| 289 |
|
| 290 |
+
<div className="flex items-center gap-1 sm:gap-4">
|
| 291 |
+
<div className="flex items-center bg-black/20 rounded-lg p-0.5 sm:p-1 border border-white/5">
|
| 292 |
+
<button onClick={handlePrev} className="p-1 sm:p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
|
| 293 |
+
<CaretLeft size={14} className="sm:w-4 sm:h-4" />
|
| 294 |
</button>
|
| 295 |
+
<button onClick={handleToday} className="hidden sm:block px-3 py-1 text-sm font-medium text-gray-300 hover:text-white transition-colors">
|
| 296 |
Today
|
| 297 |
</button>
|
| 298 |
+
<button onClick={handleNext} className="p-1 sm:p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
|
| 299 |
+
<CaretRight size={14} className="sm:w-4 sm:h-4" />
|
| 300 |
</button>
|
| 301 |
</div>
|
| 302 |
|
| 303 |
+
<div className="hidden sm:flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
|
| 304 |
{(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
|
| 305 |
<button
|
| 306 |
key={v}
|
|
|
|
| 315 |
))}
|
| 316 |
</div>
|
| 317 |
|
| 318 |
+
{/* Mobile view selector */}
|
| 319 |
+
<select
|
| 320 |
+
value={view}
|
| 321 |
+
onChange={(e) => setView(e.target.value as ViewMode)}
|
| 322 |
+
className="sm:hidden bg-black/20 border border-white/5 rounded-lg px-2 py-1 text-xs text-gray-300 focus:outline-none focus:ring-1 focus:ring-white/20"
|
| 323 |
+
>
|
| 324 |
+
<option value="day">Day</option>
|
| 325 |
+
<option value="week">Week</option>
|
| 326 |
+
<option value="month">Month</option>
|
| 327 |
+
<option value="year">Year</option>
|
| 328 |
+
</select>
|
| 329 |
+
|
| 330 |
+
<button className="hidden sm:block p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
|
| 331 |
<Plus size={20} />
|
| 332 |
</button>
|
| 333 |
+
<button className="hidden sm:block p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
|
| 334 |
<MagnifyingGlass size={20} />
|
| 335 |
</button>
|
| 336 |
</div>
|
|
|
|
| 348 |
transition={{ duration: 0.2 }}
|
| 349 |
className="h-full flex flex-col overflow-auto [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2"
|
| 350 |
>
|
| 351 |
+
<div className="flex-1 flex flex-col">
|
| 352 |
<div className="grid grid-cols-7 border-b border-white/5 bg-black/10 sticky top-0 z-10 backdrop-blur-sm">
|
| 353 |
{weekDays.map(day => (
|
| 354 |
+
<div key={day} className="py-1 sm:py-2 text-center sm:text-right pr-1 sm:pr-4 text-[10px] sm:text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 355 |
+
<span className="hidden sm:inline">{day}</span>
|
| 356 |
+
<span className="sm:hidden">{day.charAt(0)}</span>
|
| 357 |
</div>
|
| 358 |
))}
|
| 359 |
</div>
|
|
|
|
| 367 |
<div
|
| 368 |
key={index}
|
| 369 |
className={`
|
| 370 |
+
border-b border-r border-white/5 p-0.5 sm:p-1 relative group transition-colors min-h-[50px] sm:min-h-[80px]
|
| 371 |
${!dateObj.isCurrentMonth ? 'bg-black/20 text-gray-600' : 'hover:bg-white/5'}
|
| 372 |
`}
|
| 373 |
onClick={() => {
|
|
|
|
| 375 |
setView('day')
|
| 376 |
}}
|
| 377 |
>
|
| 378 |
+
<div className="flex justify-center sm:justify-end mb-0.5 sm:mb-1">
|
| 379 |
<span
|
| 380 |
className={`
|
| 381 |
+
text-xs sm:text-sm font-medium w-5 h-5 sm:w-7 sm:h-7 flex items-center justify-center rounded-full
|
| 382 |
${isCurrentDay
|
| 383 |
? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
|
| 384 |
: dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
|
|
|
|
| 387 |
{dateObj.day}
|
| 388 |
</span>
|
| 389 |
</div>
|
| 390 |
+
<div className="hidden sm:block space-y-0.5 overflow-y-auto max-h-[calc(100%-1.5rem)] [&::-webkit-scrollbar]:hidden px-0.5">
|
| 391 |
+
{dayEvents.slice(0, 3).map((event, i) => (
|
| 392 |
<div
|
| 393 |
key={i}
|
| 394 |
className={`
|
| 395 |
+
text-[9px] px-1 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
|
| 396 |
${event.color || 'bg-blue-500'} text-white font-medium shadow-sm backdrop-blur-sm bg-opacity-80
|
| 397 |
`}
|
| 398 |
+
title={event.title}
|
| 399 |
>
|
| 400 |
{event.title}
|
| 401 |
</div>
|
| 402 |
))}
|
| 403 |
+
{dayEvents.length > 3 && (
|
| 404 |
+
<div className="text-[8px] text-gray-400 px-1">
|
| 405 |
+
+{dayEvents.length - 3} more
|
| 406 |
+
</div>
|
| 407 |
+
)}
|
| 408 |
</div>
|
| 409 |
</div>
|
| 410 |
)
|
app/components/FlutterRunner.tsx
CHANGED
|
@@ -27,8 +27,7 @@ interface FileNode {
|
|
| 27 |
isOpen?: boolean
|
| 28 |
}
|
| 29 |
|
| 30 |
-
|
| 31 |
-
const [code, setCode] = useState(initialCode || `import 'package:flutter/material.dart';
|
| 32 |
|
| 33 |
void main() {
|
| 34 |
runApp(const MyApp());
|
|
@@ -95,53 +94,146 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 95 |
),
|
| 96 |
);
|
| 97 |
}
|
| 98 |
-
}`
|
| 99 |
|
|
|
|
|
|
|
| 100 |
const [key, setKey] = useState(0)
|
| 101 |
const [showFiles, setShowFiles] = useState(true)
|
| 102 |
-
const [files, setFiles] = useState<FileNode[]>([
|
| 103 |
-
{
|
| 104 |
-
id: 'root',
|
| 105 |
-
name: 'lib',
|
| 106 |
-
type: 'folder',
|
| 107 |
-
isOpen: true,
|
| 108 |
-
children: [
|
| 109 |
-
{ id: 'main', name: 'main.dart', type: 'file', content: code }
|
| 110 |
-
]
|
| 111 |
-
}
|
| 112 |
-
])
|
| 113 |
const [activeFileId, setActiveFileId] = useState('main')
|
|
|
|
| 114 |
const [lastSaved, setLastSaved] = useState<Date | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
// Auto-save to file every 2 seconds when code changes
|
| 117 |
useEffect(() => {
|
|
|
|
|
|
|
| 118 |
const saveTimer = setTimeout(async () => {
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
key: passkey,
|
| 131 |
-
action: 'save',
|
| 132 |
-
fileName: fileName,
|
| 133 |
-
content: code
|
| 134 |
-
})
|
| 135 |
})
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
}
|
| 141 |
}, 2000)
|
| 142 |
|
| 143 |
return () => clearTimeout(saveTimer)
|
| 144 |
-
}, [code,
|
| 145 |
|
| 146 |
const handleRun = () => {
|
| 147 |
setKey(prev => prev + 1)
|
|
@@ -152,7 +244,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 152 |
const url = URL.createObjectURL(blob)
|
| 153 |
const a = document.createElement('a')
|
| 154 |
a.href = url
|
| 155 |
-
a.download = 'main.dart'
|
| 156 |
a.click()
|
| 157 |
URL.revokeObjectURL(url)
|
| 158 |
}
|
|
@@ -171,6 +263,30 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 171 |
y={40}
|
| 172 |
className="flutter-ide-window"
|
| 173 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 175 |
{/* Code Editor - Left Side */}
|
| 176 |
<div className="w-[600px] flex flex-col border-r border-[#333]">
|
|
@@ -187,7 +303,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 187 |
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 188 |
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 189 |
<FileCode size={14} className="text-blue-400" />
|
| 190 |
-
|
| 191 |
</div>
|
| 192 |
{lastSaved && (
|
| 193 |
<span className="text-xs text-green-400">
|
|
@@ -278,7 +394,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 278 |
{file.children?.map(child => (
|
| 279 |
<div
|
| 280 |
key={child.id}
|
| 281 |
-
onClick={() =>
|
| 282 |
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 283 |
}`}
|
| 284 |
>
|
|
@@ -294,6 +410,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|
| 294 |
</div>
|
| 295 |
)}
|
| 296 |
</div>
|
|
|
|
| 297 |
</Window>
|
| 298 |
)
|
| 299 |
}
|
|
|
|
| 27 |
isOpen?: boolean
|
| 28 |
}
|
| 29 |
|
| 30 |
+
const DEFAULT_FLUTTER_CODE = `import 'package:flutter/material.dart';
|
|
|
|
| 31 |
|
| 32 |
void main() {
|
| 33 |
runApp(const MyApp());
|
|
|
|
| 94 |
),
|
| 95 |
);
|
| 96 |
}
|
| 97 |
+
}`
|
| 98 |
|
| 99 |
+
export function FlutterRunner({ onClose, onMinimize, onMaximize, initialCode }: FlutterRunnerProps) {
|
| 100 |
+
const [code, setCode] = useState(initialCode || DEFAULT_FLUTTER_CODE)
|
| 101 |
const [key, setKey] = useState(0)
|
| 102 |
const [showFiles, setShowFiles] = useState(true)
|
| 103 |
+
const [files, setFiles] = useState<FileNode[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
const [activeFileId, setActiveFileId] = useState('main')
|
| 105 |
+
const [activeFileName, setActiveFileName] = useState('main.dart')
|
| 106 |
const [lastSaved, setLastSaved] = useState<Date | null>(null)
|
| 107 |
+
const [passkey, setPasskey] = useState('')
|
| 108 |
+
const [isUnlocked, setIsUnlocked] = useState(false)
|
| 109 |
+
const [tempPasskey, setTempPasskey] = useState('')
|
| 110 |
+
const [loading, setLoading] = useState(false)
|
| 111 |
+
|
| 112 |
+
// Load files from secure storage
|
| 113 |
+
const loadFiles = async (key: string) => {
|
| 114 |
+
setLoading(true)
|
| 115 |
+
try {
|
| 116 |
+
const response = await fetch(`/api/data?key=${encodeURIComponent(key)}&folder=`)
|
| 117 |
+
const data = await response.json()
|
| 118 |
+
|
| 119 |
+
if (data.error) {
|
| 120 |
+
throw new Error(data.error)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Filter for .dart files
|
| 124 |
+
const dartFiles = data.files?.filter((f: any) => f.name.endsWith('.dart')) || []
|
| 125 |
+
|
| 126 |
+
if (dartFiles.length > 0) {
|
| 127 |
+
const fileNodes: FileNode[] = dartFiles.map((file: any, index: number) => ({
|
| 128 |
+
id: `file_${index}`,
|
| 129 |
+
name: file.name,
|
| 130 |
+
type: 'file' as const,
|
| 131 |
+
content: file.content
|
| 132 |
+
}))
|
| 133 |
+
|
| 134 |
+
setFiles([{
|
| 135 |
+
id: 'root',
|
| 136 |
+
name: 'Dart Files',
|
| 137 |
+
type: 'folder',
|
| 138 |
+
isOpen: true,
|
| 139 |
+
children: fileNodes
|
| 140 |
+
}])
|
| 141 |
+
|
| 142 |
+
// Load first file
|
| 143 |
+
if (fileNodes.length > 0) {
|
| 144 |
+
setActiveFileId(fileNodes[0].id)
|
| 145 |
+
setActiveFileName(fileNodes[0].name)
|
| 146 |
+
setCode(fileNodes[0].content || DEFAULT_FLUTTER_CODE)
|
| 147 |
+
}
|
| 148 |
+
} else {
|
| 149 |
+
// No files found, create default
|
| 150 |
+
setFiles([{
|
| 151 |
+
id: 'root',
|
| 152 |
+
name: 'lib',
|
| 153 |
+
type: 'folder',
|
| 154 |
+
isOpen: true,
|
| 155 |
+
children: [
|
| 156 |
+
{ id: 'main', name: 'main.dart', type: 'file', content: DEFAULT_FLUTTER_CODE }
|
| 157 |
+
]
|
| 158 |
+
}])
|
| 159 |
+
setCode(DEFAULT_FLUTTER_CODE)
|
| 160 |
+
setActiveFileName('main.dart')
|
| 161 |
+
}
|
| 162 |
+
} catch (err) {
|
| 163 |
+
console.error('Error loading files:', err)
|
| 164 |
+
// Create default on error
|
| 165 |
+
setFiles([{
|
| 166 |
+
id: 'root',
|
| 167 |
+
name: 'lib',
|
| 168 |
+
type: 'folder',
|
| 169 |
+
isOpen: true,
|
| 170 |
+
children: [
|
| 171 |
+
{ id: 'main', name: 'main.dart', type: 'file', content: DEFAULT_FLUTTER_CODE }
|
| 172 |
+
]
|
| 173 |
+
}])
|
| 174 |
+
setCode(DEFAULT_FLUTTER_CODE)
|
| 175 |
+
setActiveFileName('main.dart')
|
| 176 |
+
} finally {
|
| 177 |
+
setLoading(false)
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
const handleUnlock = async () => {
|
| 182 |
+
if (tempPasskey.trim().length >= 4) {
|
| 183 |
+
setPasskey(tempPasskey.trim())
|
| 184 |
+
setIsUnlocked(true)
|
| 185 |
+
await loadFiles(tempPasskey.trim())
|
| 186 |
+
} else {
|
| 187 |
+
alert('Passkey must be at least 4 characters')
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
const handleFileClick = (file: FileNode) => {
|
| 192 |
+
if (file.type === 'file') {
|
| 193 |
+
setActiveFileId(file.id)
|
| 194 |
+
setActiveFileName(file.name)
|
| 195 |
+
setCode(file.content || '')
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// Check for initial passkey on mount
|
| 200 |
+
useEffect(() => {
|
| 201 |
+
const sessionPasskey = sessionStorage.getItem('currentPasskey')
|
| 202 |
+
if (sessionPasskey) {
|
| 203 |
+
setPasskey(sessionPasskey)
|
| 204 |
+
setIsUnlocked(true)
|
| 205 |
+
loadFiles(sessionPasskey)
|
| 206 |
+
} else if (initialCode) {
|
| 207 |
+
setCode(initialCode)
|
| 208 |
+
}
|
| 209 |
+
}, [initialCode])
|
| 210 |
|
| 211 |
// Auto-save to file every 2 seconds when code changes
|
| 212 |
useEffect(() => {
|
| 213 |
+
if (!passkey || !isUnlocked || !activeFileName) return
|
| 214 |
+
|
| 215 |
const saveTimer = setTimeout(async () => {
|
| 216 |
+
try {
|
| 217 |
+
// Save to secure data
|
| 218 |
+
await fetch('/api/data', {
|
| 219 |
+
method: 'POST',
|
| 220 |
+
headers: { 'Content-Type': 'application/json' },
|
| 221 |
+
body: JSON.stringify({
|
| 222 |
+
key: passkey,
|
| 223 |
+
passkey: passkey,
|
| 224 |
+
action: 'save_file',
|
| 225 |
+
fileName: activeFileName,
|
| 226 |
+
content: code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
})
|
| 228 |
+
})
|
| 229 |
+
setLastSaved(new Date())
|
| 230 |
+
} catch (error) {
|
| 231 |
+
console.error('Auto-save failed:', error)
|
| 232 |
}
|
| 233 |
}, 2000)
|
| 234 |
|
| 235 |
return () => clearTimeout(saveTimer)
|
| 236 |
+
}, [code, passkey, activeFileName, isUnlocked])
|
| 237 |
|
| 238 |
const handleRun = () => {
|
| 239 |
setKey(prev => prev + 1)
|
|
|
|
| 244 |
const url = URL.createObjectURL(blob)
|
| 245 |
const a = document.createElement('a')
|
| 246 |
a.href = url
|
| 247 |
+
a.download = activeFileName || 'main.dart'
|
| 248 |
a.click()
|
| 249 |
URL.revokeObjectURL(url)
|
| 250 |
}
|
|
|
|
| 263 |
y={40}
|
| 264 |
className="flutter-ide-window"
|
| 265 |
>
|
| 266 |
+
{!isUnlocked ? (
|
| 267 |
+
<div className="flex h-full bg-[#1e1e1e] items-center justify-center">
|
| 268 |
+
<div className="bg-[#252526] p-8 rounded-lg shadow-xl border border-[#333] max-w-md w-full">
|
| 269 |
+
<h2 className="text-xl font-bold text-white mb-4">Enter Passkey</h2>
|
| 270 |
+
<p className="text-gray-400 mb-6">Enter your passkey to access Dart/Flutter files</p>
|
| 271 |
+
<input
|
| 272 |
+
type="password"
|
| 273 |
+
value={tempPasskey}
|
| 274 |
+
onChange={(e) => setTempPasskey(e.target.value)}
|
| 275 |
+
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
|
| 276 |
+
placeholder="Your passkey"
|
| 277 |
+
className="w-full px-4 py-2 bg-[#1e1e1e] border border-[#333] rounded text-white focus:outline-none focus:border-blue-500"
|
| 278 |
+
autoFocus
|
| 279 |
+
/>
|
| 280 |
+
<button
|
| 281 |
+
onClick={handleUnlock}
|
| 282 |
+
disabled={loading}
|
| 283 |
+
className="w-full mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-medium disabled:opacity-50"
|
| 284 |
+
>
|
| 285 |
+
{loading ? 'Loading...' : 'Unlock'}
|
| 286 |
+
</button>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
) : (
|
| 290 |
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 291 |
{/* Code Editor - Left Side */}
|
| 292 |
<div className="w-[600px] flex flex-col border-r border-[#333]">
|
|
|
|
| 303 |
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 304 |
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 305 |
<FileCode size={14} className="text-blue-400" />
|
| 306 |
+
{activeFileName}
|
| 307 |
</div>
|
| 308 |
{lastSaved && (
|
| 309 |
<span className="text-xs text-green-400">
|
|
|
|
| 394 |
{file.children?.map(child => (
|
| 395 |
<div
|
| 396 |
key={child.id}
|
| 397 |
+
onClick={() => handleFileClick(child)}
|
| 398 |
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 399 |
}`}
|
| 400 |
>
|
|
|
|
| 410 |
</div>
|
| 411 |
)}
|
| 412 |
</div>
|
| 413 |
+
)}
|
| 414 |
</Window>
|
| 415 |
)
|
| 416 |
}
|
app/components/LaTeXEditor.tsx
CHANGED
|
@@ -58,60 +58,146 @@ Here is an equation:
|
|
| 58 |
export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }: LaTeXEditorProps) {
|
| 59 |
const [code, setCode] = useState(initialContent || DEFAULT_LATEX)
|
| 60 |
const [showSidebar, setShowSidebar] = useState(true)
|
| 61 |
-
const [files, setFiles] = useState<FileNode[]>([
|
| 62 |
-
{
|
| 63 |
-
id: 'root',
|
| 64 |
-
name: 'Project',
|
| 65 |
-
type: 'folder',
|
| 66 |
-
isOpen: true,
|
| 67 |
-
children: [
|
| 68 |
-
{ id: 'main', name: 'main.tex', type: 'file', content: code }
|
| 69 |
-
]
|
| 70 |
-
}
|
| 71 |
-
])
|
| 72 |
const [activeFileId, setActiveFileId] = useState('main')
|
|
|
|
| 73 |
const previewRef = useRef<HTMLDivElement>(null)
|
| 74 |
const [lastSaved, setLastSaved] = useState<Date | null>(null)
|
| 75 |
const [isSaving, setIsSaving] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
// Update code if initialContent changes (e.g. opening a new file)
|
| 78 |
useEffect(() => {
|
| 79 |
if (initialContent) {
|
| 80 |
setCode(initialContent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
}, [initialContent])
|
| 83 |
|
| 84 |
// Auto-save functionality
|
| 85 |
useEffect(() => {
|
| 86 |
-
|
| 87 |
-
const passkey = sessionStorage.getItem('currentPasskey')
|
| 88 |
-
const fileName = sessionStorage.getItem('currentFileName')
|
| 89 |
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
})
|
| 103 |
})
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
}
|
| 111 |
}, 2000) // Debounce 2s
|
| 112 |
|
| 113 |
return () => clearTimeout(saveTimer)
|
| 114 |
-
}, [code])
|
| 115 |
|
| 116 |
// Render LaTeX preview
|
| 117 |
useEffect(() => {
|
|
@@ -164,7 +250,7 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 164 |
const url = URL.createObjectURL(blob)
|
| 165 |
const a = document.createElement('a')
|
| 166 |
a.href = url
|
| 167 |
-
a.download = 'main.tex'
|
| 168 |
a.click()
|
| 169 |
URL.revokeObjectURL(url)
|
| 170 |
}
|
|
@@ -183,6 +269,30 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 183 |
y={60}
|
| 184 |
className="latex-studio-window"
|
| 185 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 187 |
{/* Sidebar */}
|
| 188 |
{showSidebar && (
|
|
@@ -206,7 +316,7 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 206 |
{file.children?.map(child => (
|
| 207 |
<div
|
| 208 |
key={child.id}
|
| 209 |
-
onClick={() =>
|
| 210 |
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 211 |
}`}
|
| 212 |
>
|
|
@@ -237,7 +347,7 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 237 |
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 238 |
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 239 |
<FileText size={14} className="text-green-400" />
|
| 240 |
-
|
| 241 |
</div>
|
| 242 |
{lastSaved && (
|
| 243 |
<div className="flex items-center gap-1 text-xs text-gray-500 ml-2">
|
|
@@ -319,6 +429,7 @@ export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }:
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
</div>
|
|
|
|
| 322 |
</Window>
|
| 323 |
)
|
| 324 |
}
|
|
|
|
| 58 |
export function LaTeXEditor({ onClose, onMinimize, onMaximize, initialContent }: LaTeXEditorProps) {
|
| 59 |
const [code, setCode] = useState(initialContent || DEFAULT_LATEX)
|
| 60 |
const [showSidebar, setShowSidebar] = useState(true)
|
| 61 |
+
const [files, setFiles] = useState<FileNode[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
const [activeFileId, setActiveFileId] = useState('main')
|
| 63 |
+
const [activeFileName, setActiveFileName] = useState('main.tex')
|
| 64 |
const previewRef = useRef<HTMLDivElement>(null)
|
| 65 |
const [lastSaved, setLastSaved] = useState<Date | null>(null)
|
| 66 |
const [isSaving, setIsSaving] = useState(false)
|
| 67 |
+
const [passkey, setPasskey] = useState('')
|
| 68 |
+
const [isUnlocked, setIsUnlocked] = useState(false)
|
| 69 |
+
const [tempPasskey, setTempPasskey] = useState('')
|
| 70 |
+
const [loading, setLoading] = useState(false)
|
| 71 |
+
|
| 72 |
+
// Load files from secure storage
|
| 73 |
+
const loadFiles = async (key: string) => {
|
| 74 |
+
setLoading(true)
|
| 75 |
+
try {
|
| 76 |
+
const response = await fetch(`/api/data?key=${encodeURIComponent(key)}&folder=`)
|
| 77 |
+
const data = await response.json()
|
| 78 |
+
|
| 79 |
+
if (data.error) {
|
| 80 |
+
throw new Error(data.error)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Filter for .tex files
|
| 84 |
+
const texFiles = data.files?.filter((f: any) => f.name.endsWith('.tex')) || []
|
| 85 |
+
|
| 86 |
+
if (texFiles.length > 0) {
|
| 87 |
+
const fileNodes: FileNode[] = texFiles.map((file: any, index: number) => ({
|
| 88 |
+
id: `file_${index}`,
|
| 89 |
+
name: file.name,
|
| 90 |
+
type: 'file' as const,
|
| 91 |
+
content: file.content
|
| 92 |
+
}))
|
| 93 |
+
|
| 94 |
+
setFiles([{
|
| 95 |
+
id: 'root',
|
| 96 |
+
name: 'LaTeX Files',
|
| 97 |
+
type: 'folder',
|
| 98 |
+
isOpen: true,
|
| 99 |
+
children: fileNodes
|
| 100 |
+
}])
|
| 101 |
+
|
| 102 |
+
// Load first file
|
| 103 |
+
if (fileNodes.length > 0) {
|
| 104 |
+
setActiveFileId(fileNodes[0].id)
|
| 105 |
+
setActiveFileName(fileNodes[0].name)
|
| 106 |
+
setCode(fileNodes[0].content || DEFAULT_LATEX)
|
| 107 |
+
}
|
| 108 |
+
} else {
|
| 109 |
+
// No files found, create default
|
| 110 |
+
setFiles([{
|
| 111 |
+
id: 'root',
|
| 112 |
+
name: 'LaTeX Files',
|
| 113 |
+
type: 'folder',
|
| 114 |
+
isOpen: true,
|
| 115 |
+
children: [
|
| 116 |
+
{ id: 'main', name: 'main.tex', type: 'file', content: DEFAULT_LATEX }
|
| 117 |
+
]
|
| 118 |
+
}])
|
| 119 |
+
setCode(DEFAULT_LATEX)
|
| 120 |
+
setActiveFileName('main.tex')
|
| 121 |
+
}
|
| 122 |
+
} catch (err) {
|
| 123 |
+
console.error('Error loading files:', err)
|
| 124 |
+
// Create default on error
|
| 125 |
+
setFiles([{
|
| 126 |
+
id: 'root',
|
| 127 |
+
name: 'LaTeX Files',
|
| 128 |
+
type: 'folder',
|
| 129 |
+
isOpen: true,
|
| 130 |
+
children: [
|
| 131 |
+
{ id: 'main', name: 'main.tex', type: 'file', content: DEFAULT_LATEX }
|
| 132 |
+
]
|
| 133 |
+
}])
|
| 134 |
+
setCode(DEFAULT_LATEX)
|
| 135 |
+
setActiveFileName('main.tex')
|
| 136 |
+
} finally {
|
| 137 |
+
setLoading(false)
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const handleUnlock = async () => {
|
| 142 |
+
if (tempPasskey.trim().length >= 4) {
|
| 143 |
+
setPasskey(tempPasskey.trim())
|
| 144 |
+
setIsUnlocked(true)
|
| 145 |
+
await loadFiles(tempPasskey.trim())
|
| 146 |
+
} else {
|
| 147 |
+
alert('Passkey must be at least 4 characters')
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
const handleFileClick = (file: FileNode) => {
|
| 152 |
+
if (file.type === 'file') {
|
| 153 |
+
setActiveFileId(file.id)
|
| 154 |
+
setActiveFileName(file.name)
|
| 155 |
+
setCode(file.content || '')
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
|
| 159 |
// Update code if initialContent changes (e.g. opening a new file)
|
| 160 |
useEffect(() => {
|
| 161 |
if (initialContent) {
|
| 162 |
setCode(initialContent)
|
| 163 |
+
// Check if we have a passkey from session
|
| 164 |
+
const sessionPasskey = sessionStorage.getItem('currentPasskey')
|
| 165 |
+
if (sessionPasskey) {
|
| 166 |
+
setPasskey(sessionPasskey)
|
| 167 |
+
setIsUnlocked(true)
|
| 168 |
+
loadFiles(sessionPasskey)
|
| 169 |
+
}
|
| 170 |
}
|
| 171 |
}, [initialContent])
|
| 172 |
|
| 173 |
// Auto-save functionality
|
| 174 |
useEffect(() => {
|
| 175 |
+
if (!passkey || !isUnlocked || !activeFileName) return
|
|
|
|
|
|
|
| 176 |
|
| 177 |
+
const saveTimer = setTimeout(async () => {
|
| 178 |
+
setIsSaving(true)
|
| 179 |
+
try {
|
| 180 |
+
await fetch('/api/data', {
|
| 181 |
+
method: 'POST',
|
| 182 |
+
headers: { 'Content-Type': 'application/json' },
|
| 183 |
+
body: JSON.stringify({
|
| 184 |
+
key: passkey,
|
| 185 |
+
passkey: passkey,
|
| 186 |
+
action: 'save_file',
|
| 187 |
+
fileName: activeFileName,
|
| 188 |
+
content: code
|
|
|
|
| 189 |
})
|
| 190 |
+
})
|
| 191 |
+
setLastSaved(new Date())
|
| 192 |
+
} catch (err) {
|
| 193 |
+
console.error('Auto-save failed:', err)
|
| 194 |
+
} finally {
|
| 195 |
+
setIsSaving(false)
|
| 196 |
}
|
| 197 |
}, 2000) // Debounce 2s
|
| 198 |
|
| 199 |
return () => clearTimeout(saveTimer)
|
| 200 |
+
}, [code, passkey, activeFileName, isUnlocked])
|
| 201 |
|
| 202 |
// Render LaTeX preview
|
| 203 |
useEffect(() => {
|
|
|
|
| 250 |
const url = URL.createObjectURL(blob)
|
| 251 |
const a = document.createElement('a')
|
| 252 |
a.href = url
|
| 253 |
+
a.download = activeFileName || 'main.tex'
|
| 254 |
a.click()
|
| 255 |
URL.revokeObjectURL(url)
|
| 256 |
}
|
|
|
|
| 269 |
y={60}
|
| 270 |
className="latex-studio-window"
|
| 271 |
>
|
| 272 |
+
{!isUnlocked ? (
|
| 273 |
+
<div className="flex h-full bg-[#1e1e1e] items-center justify-center">
|
| 274 |
+
<div className="bg-[#252526] p-8 rounded-lg shadow-xl border border-[#333] max-w-md w-full">
|
| 275 |
+
<h2 className="text-xl font-bold text-white mb-4">Enter Passkey</h2>
|
| 276 |
+
<p className="text-gray-400 mb-6">Enter your passkey to access LaTeX files</p>
|
| 277 |
+
<input
|
| 278 |
+
type="password"
|
| 279 |
+
value={tempPasskey}
|
| 280 |
+
onChange={(e) => setTempPasskey(e.target.value)}
|
| 281 |
+
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
|
| 282 |
+
placeholder="Your passkey"
|
| 283 |
+
className="w-full px-4 py-2 bg-[#1e1e1e] border border-[#333] rounded text-white focus:outline-none focus:border-blue-500"
|
| 284 |
+
autoFocus
|
| 285 |
+
/>
|
| 286 |
+
<button
|
| 287 |
+
onClick={handleUnlock}
|
| 288 |
+
disabled={loading}
|
| 289 |
+
className="w-full mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded font-medium disabled:opacity-50"
|
| 290 |
+
>
|
| 291 |
+
{loading ? 'Loading...' : 'Unlock'}
|
| 292 |
+
</button>
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
) : (
|
| 296 |
<div className="flex h-full bg-[#1e1e1e] text-gray-300 font-sans">
|
| 297 |
{/* Sidebar */}
|
| 298 |
{showSidebar && (
|
|
|
|
| 316 |
{file.children?.map(child => (
|
| 317 |
<div
|
| 318 |
key={child.id}
|
| 319 |
+
onClick={() => handleFileClick(child)}
|
| 320 |
className={`flex items-center gap-2 py-1 px-2 ml-4 text-sm rounded cursor-pointer ${activeFileId === child.id ? 'bg-[#37373d] text-white' : 'text-gray-400 hover:bg-[#2a2d2e]'
|
| 321 |
}`}
|
| 322 |
>
|
|
|
|
| 347 |
<div className="h-4 w-[1px] bg-gray-600 mx-2" />
|
| 348 |
<div className="flex items-center gap-2 px-3 py-1 bg-[#1e1e1e] rounded text-xs text-gray-400 border border-[#333]">
|
| 349 |
<FileText size={14} className="text-green-400" />
|
| 350 |
+
{activeFileName}
|
| 351 |
</div>
|
| 352 |
{lastSaved && (
|
| 353 |
<div className="flex items-center gap-1 text-xs text-gray-500 ml-2">
|
|
|
|
| 429 |
</div>
|
| 430 |
</div>
|
| 431 |
</div>
|
| 432 |
+
)}
|
| 433 |
</Window>
|
| 434 |
)
|
| 435 |
}
|
app/components/QuizApp.tsx
CHANGED
|
@@ -31,6 +31,7 @@ interface QuizAppProps {
|
|
| 31 |
}
|
| 32 |
|
| 33 |
export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
|
|
|
| 34 |
const [passkey, setPasskey] = useState('')
|
| 35 |
const [tempPasskey, setTempPasskey] = useState('')
|
| 36 |
const [isLocked, setIsLocked] = useState(true)
|
|
@@ -47,6 +48,19 @@ export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
|
| 47 |
const [completed, setCompleted] = useState(false)
|
| 48 |
const [saving, setSaving] = useState(false)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
const handlePasskeySubmit = () => {
|
| 52 |
if (tempPasskey.trim().length >= 4) {
|
|
@@ -246,11 +260,11 @@ export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
|
| 246 |
onClose={onClose}
|
| 247 |
onMinimize={onMinimize}
|
| 248 |
zIndex={zIndex}
|
| 249 |
-
width={
|
| 250 |
-
height={
|
| 251 |
className="bg-[#F5F5F7] font-sans"
|
| 252 |
>
|
| 253 |
-
<div className="h-full flex flex-col">
|
| 254 |
{/* Header */}
|
| 255 |
<div className="bg-[#F5F5F7]/80 backdrop-blur-xl border-b border-[#000000]/10 p-4 flex items-center justify-between sticky top-0 z-10">
|
| 256 |
<div className="flex items-center gap-3">
|
|
@@ -280,7 +294,7 @@ export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
|
| 280 |
</div>
|
| 281 |
|
| 282 |
{/* Content */}
|
| 283 |
-
<div className="flex-1 p-8 overflow-y-auto flex flex-col items-center justify-center">
|
| 284 |
{isLocked ? (
|
| 285 |
<div className="flex flex-col items-center gap-6 max-w-md w-full bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
| 286 |
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center">
|
|
@@ -366,72 +380,74 @@ export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
|
| 366 |
</div>
|
| 367 |
</div>
|
| 368 |
) : questions.length > 0 ? (
|
| 369 |
-
<div className="w-full max-w-2xl flex flex-col h-full justify-
|
| 370 |
{/* Question Card */}
|
| 371 |
-
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
| 372 |
-
<div className="p-
|
| 373 |
-
<h3 className="text-xl font-semibold text-gray-900 leading-
|
| 374 |
{questions[currentQuestionIndex].question}
|
| 375 |
</h3>
|
| 376 |
</div>
|
| 377 |
|
| 378 |
-
<div className="p-4 bg-gray-50/50
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
| 399 |
</div>
|
| 400 |
|
| 401 |
-
<div className="px-6 py-4 bg-white border-t border-gray-100 flex justify-between items-center">
|
| 402 |
<button
|
| 403 |
onClick={handlePrev}
|
| 404 |
disabled={currentQuestionIndex === 0}
|
| 405 |
-
className="flex items-center gap-1.5 px-3 py-1.5 text-gray-500 hover:text-gray-800 disabled:opacity-30 disabled:hover:text-gray-500 transition-colors text-sm font-medium"
|
| 406 |
>
|
| 407 |
-
<CaretLeft size={
|
| 408 |
-
Back
|
| 409 |
</button>
|
| 410 |
|
| 411 |
<button
|
| 412 |
onClick={handleNext}
|
| 413 |
disabled={!answers[questions[currentQuestionIndex]?.id]}
|
| 414 |
-
className={`flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-medium shadow-sm transition-all ${!answers[questions[currentQuestionIndex]?.id]
|
| 415 |
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
| 416 |
: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800'
|
| 417 |
}`}
|
| 418 |
>
|
| 419 |
{currentQuestionIndex === questions.length - 1 ? 'Finish' : 'Next'}
|
| 420 |
-
{currentQuestionIndex !== questions.length - 1 && <CaretRight size={
|
| 421 |
</button>
|
| 422 |
</div>
|
| 423 |
</div>
|
| 424 |
|
| 425 |
{/* Progress Indicator */}
|
| 426 |
-
<div className="mt-
|
| 427 |
{questions.map((_, idx) => (
|
| 428 |
<div
|
| 429 |
key={idx}
|
| 430 |
-
className={`h-1
|
| 431 |
-
? 'w-
|
| 432 |
: idx < currentQuestionIndex
|
| 433 |
-
? 'w-1
|
| 434 |
-
: 'w-1
|
| 435 |
}`}
|
| 436 |
/>
|
| 437 |
))}
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
export function QuizApp({ onClose, onMinimize, zIndex }: QuizAppProps) {
|
| 34 |
+
const [windowSize, setWindowSize] = useState({ width: 800, height: 600 })
|
| 35 |
const [passkey, setPasskey] = useState('')
|
| 36 |
const [tempPasskey, setTempPasskey] = useState('')
|
| 37 |
const [isLocked, setIsLocked] = useState(true)
|
|
|
|
| 48 |
const [completed, setCompleted] = useState(false)
|
| 49 |
const [saving, setSaving] = useState(false)
|
| 50 |
|
| 51 |
+
// Handle window resize
|
| 52 |
+
useEffect(() => {
|
| 53 |
+
const handleResize = () => {
|
| 54 |
+
setWindowSize({
|
| 55 |
+
width: Math.min(800, window.innerWidth - 40),
|
| 56 |
+
height: Math.min(600, window.innerHeight - 40)
|
| 57 |
+
})
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
handleResize() // Set initial size
|
| 61 |
+
window.addEventListener('resize', handleResize)
|
| 62 |
+
return () => window.removeEventListener('resize', handleResize)
|
| 63 |
+
}, [])
|
| 64 |
|
| 65 |
const handlePasskeySubmit = () => {
|
| 66 |
if (tempPasskey.trim().length >= 4) {
|
|
|
|
| 260 |
onClose={onClose}
|
| 261 |
onMinimize={onMinimize}
|
| 262 |
zIndex={zIndex}
|
| 263 |
+
width={windowSize.width}
|
| 264 |
+
height={windowSize.height}
|
| 265 |
className="bg-[#F5F5F7] font-sans"
|
| 266 |
>
|
| 267 |
+
<div className="h-full flex flex-col overflow-hidden">
|
| 268 |
{/* Header */}
|
| 269 |
<div className="bg-[#F5F5F7]/80 backdrop-blur-xl border-b border-[#000000]/10 p-4 flex items-center justify-between sticky top-0 z-10">
|
| 270 |
<div className="flex items-center gap-3">
|
|
|
|
| 294 |
</div>
|
| 295 |
|
| 296 |
{/* Content */}
|
| 297 |
+
<div className="flex-1 p-4 sm:p-6 md:p-8 overflow-y-auto flex flex-col items-center justify-center min-h-0">
|
| 298 |
{isLocked ? (
|
| 299 |
<div className="flex flex-col items-center gap-6 max-w-md w-full bg-white p-8 rounded-2xl shadow-sm border border-gray-200">
|
| 300 |
<div className="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center">
|
|
|
|
| 380 |
</div>
|
| 381 |
</div>
|
| 382 |
) : questions.length > 0 ? (
|
| 383 |
+
<div className="w-full max-w-2xl flex flex-col h-full justify-between py-2">
|
| 384 |
{/* Question Card */}
|
| 385 |
+
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex-1 flex flex-col">
|
| 386 |
+
<div className="p-3 sm:p-4 md:p-6 border-b border-gray-100 flex-shrink-0">
|
| 387 |
+
<h3 className="text-base sm:text-lg md:text-xl font-semibold text-gray-900 leading-snug">
|
| 388 |
{questions[currentQuestionIndex].question}
|
| 389 |
</h3>
|
| 390 |
</div>
|
| 391 |
|
| 392 |
+
<div className="p-3 sm:p-4 bg-gray-50/50 overflow-y-auto flex-1 min-h-0">
|
| 393 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-3">
|
| 394 |
+
{questions[currentQuestionIndex].options.map((option, idx) => {
|
| 395 |
+
const currentQId = questions[currentQuestionIndex].id
|
| 396 |
+
const isSelected = answers[currentQId] === option
|
| 397 |
+
return (
|
| 398 |
+
<button
|
| 399 |
+
key={idx}
|
| 400 |
+
onClick={() => handleOptionSelect(option)}
|
| 401 |
+
className={`text-left px-3 sm:px-4 py-2 sm:py-2.5 rounded-lg border transition-all duration-200 flex items-start gap-2 text-sm ${isSelected
|
| 402 |
+
? 'bg-blue-600 border-blue-600 text-white shadow-md shadow-blue-500/20'
|
| 403 |
+
: 'bg-white border-gray-200 hover:border-gray-300 hover:bg-gray-50 text-gray-700'
|
| 404 |
+
}`}
|
| 405 |
+
>
|
| 406 |
+
<div className={`w-4 h-4 rounded-full border flex items-center justify-center flex-shrink-0 mt-0.5 ${isSelected ? 'border-white bg-white/20' : 'border-gray-300'
|
| 407 |
+
}`}>
|
| 408 |
+
{isSelected && <div className="w-2 h-2 bg-white rounded-full" />}
|
| 409 |
+
</div>
|
| 410 |
+
<span className="font-medium break-words flex-1 leading-tight">{option}</span>
|
| 411 |
+
</button>
|
| 412 |
+
)
|
| 413 |
+
})}
|
| 414 |
+
</div>
|
| 415 |
</div>
|
| 416 |
|
| 417 |
+
<div className="px-3 sm:px-6 py-3 sm:py-4 bg-white border-t border-gray-100 flex justify-between items-center flex-shrink-0">
|
| 418 |
<button
|
| 419 |
onClick={handlePrev}
|
| 420 |
disabled={currentQuestionIndex === 0}
|
| 421 |
+
className="flex items-center gap-1 sm:gap-1.5 px-2 sm:px-3 py-1 sm:py-1.5 text-gray-500 hover:text-gray-800 disabled:opacity-30 disabled:hover:text-gray-500 transition-colors text-xs sm:text-sm font-medium"
|
| 422 |
>
|
| 423 |
+
<CaretLeft size={14} className="sm:w-4 sm:h-4" weight="bold" />
|
| 424 |
+
<span className="hidden sm:inline">Back</span>
|
| 425 |
</button>
|
| 426 |
|
| 427 |
<button
|
| 428 |
onClick={handleNext}
|
| 429 |
disabled={!answers[questions[currentQuestionIndex]?.id]}
|
| 430 |
+
className={`flex items-center gap-1 sm:gap-2 px-3 sm:px-5 py-1.5 sm:py-2 rounded-lg text-xs sm:text-sm font-medium shadow-sm transition-all ${!answers[questions[currentQuestionIndex]?.id]
|
| 431 |
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
| 432 |
: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800'
|
| 433 |
}`}
|
| 434 |
>
|
| 435 |
{currentQuestionIndex === questions.length - 1 ? 'Finish' : 'Next'}
|
| 436 |
+
{currentQuestionIndex !== questions.length - 1 && <CaretRight size={14} className="sm:w-4 sm:h-4" weight="bold" />}
|
| 437 |
</button>
|
| 438 |
</div>
|
| 439 |
</div>
|
| 440 |
|
| 441 |
{/* Progress Indicator */}
|
| 442 |
+
<div className="mt-2 sm:mt-3 flex items-center justify-center gap-1 flex-shrink-0">
|
| 443 |
{questions.map((_, idx) => (
|
| 444 |
<div
|
| 445 |
key={idx}
|
| 446 |
+
className={`h-1 rounded-full transition-all duration-300 ${idx === currentQuestionIndex
|
| 447 |
+
? 'w-6 bg-blue-600'
|
| 448 |
: idx < currentQuestionIndex
|
| 449 |
+
? 'w-1 bg-gray-300'
|
| 450 |
+
: 'w-1 bg-gray-200'
|
| 451 |
}`}
|
| 452 |
/>
|
| 453 |
))}
|