Spaces:
Running
Running
few changes like calendar and clock
Browse files- app/components/Calendar.tsx +453 -174
- app/components/Desktop.tsx +238 -66
- app/components/Dock.tsx +47 -26
- app/components/DraggableDesktopIcon.tsx +12 -12
- app/components/DynamicCalendarIcon.tsx +42 -0
- app/components/DynamicClockIcon.tsx +66 -0
- app/components/FileManager.tsx +44 -11
- app/components/FlutterCodeEditor.tsx +1 -1
- app/components/FlutterRunner.tsx +2 -1
- app/components/LaTeXEditor.tsx +1 -1
- app/components/Window.tsx +37 -37
- app/data/holidays.ts +126 -0
app/components/Calendar.tsx
CHANGED
|
@@ -1,10 +1,19 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import React, { useState } from 'react'
|
| 4 |
-
import {
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import { useKV } from '../hooks/useKV'
|
| 7 |
import Window from './Window'
|
|
|
|
| 8 |
|
| 9 |
interface CalendarProps {
|
| 10 |
onClose: () => void
|
|
@@ -12,95 +21,181 @@ interface CalendarProps {
|
|
| 12 |
onMaximize?: () => void
|
| 13 |
}
|
| 14 |
|
| 15 |
-
interface
|
| 16 |
-
|
| 17 |
-
date: string
|
| 18 |
-
title: string
|
| 19 |
-
type: 'holiday' | 'custom'
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
{ id: '1', date: '2025-01-01', title: 'New Year\'s Day', type: 'holiday' },
|
| 24 |
-
{ id: '2', date: '2025-02-14', title: 'Valentine\'s Day', type: 'holiday' },
|
| 25 |
-
{ id: '3', date: '2025-03-17', title: 'St. Patrick\'s Day', type: 'holiday' },
|
| 26 |
-
{ id: '4', date: '2025-04-20', title: 'Easter Sunday', type: 'holiday' },
|
| 27 |
-
{ id: '5', date: '2025-05-11', title: 'Mother\'s Day', type: 'holiday' },
|
| 28 |
-
{ id: '6', date: '2025-06-15', title: 'Father\'s Day', type: 'holiday' },
|
| 29 |
-
{ id: '7', date: '2025-07-04', title: 'Independence Day', type: 'holiday' },
|
| 30 |
-
{ id: '8', date: '2025-10-31', title: 'Halloween', type: 'holiday' },
|
| 31 |
-
{ id: '9', date: '2025-11-27', title: 'Thanksgiving', type: 'holiday' },
|
| 32 |
-
{ id: '10', date: '2025-12-25', title: 'Christmas Day', type: 'holiday' },
|
| 33 |
-
{ id: '11', date: '2025-12-31', title: 'New Year\'s Eve', type: 'holiday' },
|
| 34 |
-
]
|
| 35 |
|
| 36 |
export function Calendar({ onClose, onMinimize, onMaximize }: CalendarProps) {
|
| 37 |
const [currentDate, setCurrentDate] = useState(new Date())
|
| 38 |
-
const [
|
| 39 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
const monthNames = [
|
| 42 |
'January', 'February', 'March', 'April', 'May', 'June',
|
| 43 |
'July', 'August', 'September', 'October', 'November', 'December'
|
| 44 |
]
|
| 45 |
|
| 46 |
-
const
|
|
|
|
| 47 |
|
| 48 |
-
const getDaysInMonth = (
|
| 49 |
-
const year = date.getFullYear()
|
| 50 |
-
const month = date.getMonth()
|
| 51 |
const firstDay = new Date(year, month, 1)
|
| 52 |
const lastDay = new Date(year, month + 1, 0)
|
| 53 |
const daysInMonth = lastDay.getDate()
|
| 54 |
-
const startingDayOfWeek = firstDay.getDay()
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
|
|
|
|
|
|
|
|
|
| 60 |
for (let i = 1; i <= daysInMonth; i++) {
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
-
const
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
currentDate.getMonth() === today.getMonth() &&
|
| 73 |
-
currentDate.getFullYear() === today.getFullYear()
|
| 74 |
-
)
|
| 75 |
}
|
| 76 |
|
| 77 |
-
const
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
return events.filter(event => event.date === dateStr)
|
| 81 |
}
|
| 82 |
|
| 83 |
-
const
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
-
const
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
-
const
|
| 93 |
-
setCurrentDate(new Date(
|
| 94 |
-
setSelectedDay(null)
|
| 95 |
}
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
|
|
|
| 101 |
}
|
| 102 |
|
| 103 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
return (
|
| 106 |
<Window
|
|
@@ -110,141 +205,325 @@ export function Calendar({ onClose, onMinimize, onMaximize }: CalendarProps) {
|
|
| 110 |
onClose={onClose}
|
| 111 |
onMinimize={onMinimize}
|
| 112 |
onMaximize={onMaximize}
|
| 113 |
-
width={
|
| 114 |
-
height={
|
| 115 |
-
x={
|
| 116 |
-
y={
|
| 117 |
className="calendar-window"
|
| 118 |
>
|
| 119 |
-
<div className="
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
className="
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
</div>
|
| 130 |
-
<button
|
| 131 |
-
onClick={nextMonth}
|
| 132 |
-
className="p-2 hover:bg-[#f0f0f0] rounded"
|
| 133 |
-
>
|
| 134 |
-
<CaretRight size={20} weight="bold" />
|
| 135 |
-
</button>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
>
|
| 144 |
-
{
|
| 145 |
-
</
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
</div>
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
</div>
|
| 175 |
-
)}
|
| 176 |
-
</
|
| 177 |
-
) : (
|
| 178 |
-
<div
|
| 179 |
-
key={index}
|
| 180 |
-
className="aspect-square"
|
| 181 |
-
/>
|
| 182 |
-
)
|
| 183 |
-
))}
|
| 184 |
-
</div>
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
</div>
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
</div>
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
</div>
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
</div>
|
| 210 |
-
|
| 211 |
-
</div>
|
| 212 |
-
</div>
|
| 213 |
-
)}
|
| 214 |
-
|
| 215 |
-
{!selectedDay && (
|
| 216 |
-
<div className="mt-4 pt-4 border-t border-[#e0e0e0] flex-1 overflow-y-auto">
|
| 217 |
-
<div className="text-xs text-[#666] mb-2">Today</div>
|
| 218 |
-
<div className="text-sm font-medium text-[#2c2c2c]">
|
| 219 |
-
{today.toLocaleDateString('en-US', {
|
| 220 |
-
weekday: 'long',
|
| 221 |
-
year: 'numeric',
|
| 222 |
-
month: 'long',
|
| 223 |
-
day: 'numeric'
|
| 224 |
-
})}
|
| 225 |
</div>
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
| 233 |
<div
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</div>
|
| 241 |
</div>
|
| 242 |
-
|
| 243 |
-
|
| 244 |
</div>
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
|
|
|
| 248 |
</div>
|
| 249 |
</Window>
|
| 250 |
)
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import React, { useState, useEffect, useMemo, useRef } from 'react'
|
| 4 |
+
import {
|
| 5 |
+
CaretLeft,
|
| 6 |
+
CaretRight,
|
| 7 |
+
Plus,
|
| 8 |
+
MagnifyingGlass,
|
| 9 |
+
List,
|
| 10 |
+
CalendarBlank,
|
| 11 |
+
Clock
|
| 12 |
+
} from '@phosphor-icons/react'
|
| 13 |
+
import { motion, AnimatePresence } from 'framer-motion'
|
| 14 |
import { useKV } from '../hooks/useKV'
|
| 15 |
import Window from './Window'
|
| 16 |
+
import { recurringHolidays, getVariableHolidays, Holiday } from '../data/holidays'
|
| 17 |
|
| 18 |
interface CalendarProps {
|
| 19 |
onClose: () => void
|
|
|
|
| 21 |
onMaximize?: () => void
|
| 22 |
}
|
| 23 |
|
| 24 |
+
interface CalendarEvent extends Holiday {
|
| 25 |
+
isCustom?: boolean
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
+
type ViewMode = 'day' | 'week' | 'month' | 'year'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
export function Calendar({ onClose, onMinimize, onMaximize }: CalendarProps) {
|
| 31 |
const [currentDate, setCurrentDate] = useState(new Date())
|
| 32 |
+
const [view, setView] = useState<ViewMode>('month')
|
| 33 |
+
const [selectedDate, setSelectedDate] = useState<Date | null>(null)
|
| 34 |
+
const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
|
| 35 |
+
const [searchQuery, setSearchQuery] = useState('')
|
| 36 |
+
const [isSearchOpen, setIsSearchOpen] = useState(false)
|
| 37 |
+
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
| 38 |
+
|
| 39 |
+
// Scroll to 8 AM on mount for day/week views
|
| 40 |
+
useEffect(() => {
|
| 41 |
+
if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
|
| 42 |
+
// 8 AM is the 8th hour slot, assuming 60px height per hour
|
| 43 |
+
scrollContainerRef.current.scrollTop = 8 * 60
|
| 44 |
+
}
|
| 45 |
+
}, [view])
|
| 46 |
+
|
| 47 |
+
// Generate all events for the current view's year
|
| 48 |
+
const allEvents = useMemo(() => {
|
| 49 |
+
const year = currentDate.getFullYear()
|
| 50 |
+
// Get holidays for previous, current, and next year to handle boundary transitions
|
| 51 |
+
const years = [year - 1, year, year + 1]
|
| 52 |
+
let events: CalendarEvent[] = [...customEvents]
|
| 53 |
+
|
| 54 |
+
years.forEach(y => {
|
| 55 |
+
const variable = getVariableHolidays(y)
|
| 56 |
+
const recurring = recurringHolidays.map(h => ({
|
| 57 |
+
...h,
|
| 58 |
+
date: `${y}-${h.date}`
|
| 59 |
+
}))
|
| 60 |
+
events = [...events, ...variable, ...recurring]
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
return events
|
| 64 |
+
}, [currentDate.getFullYear(), customEvents])
|
| 65 |
|
| 66 |
const monthNames = [
|
| 67 |
'January', 'February', 'March', 'April', 'May', 'June',
|
| 68 |
'July', 'August', 'September', 'October', 'November', 'December'
|
| 69 |
]
|
| 70 |
|
| 71 |
+
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
| 72 |
+
const hours = Array.from({ length: 24 }, (_, i) => i)
|
| 73 |
|
| 74 |
+
const getDaysInMonth = (year: number, month: number) => {
|
|
|
|
|
|
|
| 75 |
const firstDay = new Date(year, month, 1)
|
| 76 |
const lastDay = new Date(year, month + 1, 0)
|
| 77 |
const daysInMonth = lastDay.getDate()
|
| 78 |
+
const startingDayOfWeek = firstDay.getDay() // 0 = Sunday
|
| 79 |
|
| 80 |
+
// Previous month days to fill the grid
|
| 81 |
+
const prevMonthDays = []
|
| 82 |
+
const prevMonthLastDay = new Date(year, month, 0).getDate()
|
| 83 |
+
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
| 84 |
+
prevMonthDays.push({
|
| 85 |
+
day: prevMonthLastDay - i,
|
| 86 |
+
month: month - 1,
|
| 87 |
+
year: year,
|
| 88 |
+
isCurrentMonth: false
|
| 89 |
+
})
|
| 90 |
}
|
| 91 |
+
|
| 92 |
+
// Current month days
|
| 93 |
+
const currentMonthDays = []
|
| 94 |
for (let i = 1; i <= daysInMonth; i++) {
|
| 95 |
+
currentMonthDays.push({
|
| 96 |
+
day: i,
|
| 97 |
+
month: month,
|
| 98 |
+
year: year,
|
| 99 |
+
isCurrentMonth: true
|
| 100 |
+
})
|
| 101 |
}
|
| 102 |
+
|
| 103 |
+
// Next month days to complete the grid (6 rows * 7 cols = 42 cells usually)
|
| 104 |
+
const nextMonthDays = []
|
| 105 |
+
const totalDaysSoFar = prevMonthDays.length + currentMonthDays.length
|
| 106 |
+
const remainingCells = 42 - totalDaysSoFar // Ensure 6 rows
|
| 107 |
+
|
| 108 |
+
for (let i = 1; i <= remainingCells; i++) {
|
| 109 |
+
nextMonthDays.push({
|
| 110 |
+
day: i,
|
| 111 |
+
month: month + 1,
|
| 112 |
+
year: year,
|
| 113 |
+
isCurrentMonth: false
|
| 114 |
+
})
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return [...prevMonthDays, ...currentMonthDays, ...nextMonthDays]
|
| 118 |
}
|
| 119 |
|
| 120 |
+
const calendarDays = useMemo(() => {
|
| 121 |
+
return getDaysInMonth(currentDate.getFullYear(), currentDate.getMonth())
|
| 122 |
+
}, [currentDate])
|
| 123 |
+
|
| 124 |
+
const getEventsForDate = (dateStr: string) => {
|
| 125 |
+
return allEvents.filter(e => e.date === dateStr)
|
|
|
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
+
const isToday = (day: number, month: number, year: number) => {
|
| 129 |
+
const today = new Date()
|
| 130 |
+
return day === today.getDate() && month === today.getMonth() && year === today.getFullYear()
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
+
const handlePrev = () => {
|
| 134 |
+
const newDate = new Date(currentDate)
|
| 135 |
+
if (view === 'month') {
|
| 136 |
+
newDate.setMonth(newDate.getMonth() - 1)
|
| 137 |
+
} else if (view === 'year') {
|
| 138 |
+
newDate.setFullYear(newDate.getFullYear() - 1)
|
| 139 |
+
} else if (view === 'week') {
|
| 140 |
+
newDate.setDate(newDate.getDate() - 7)
|
| 141 |
+
} else if (view === 'day') {
|
| 142 |
+
newDate.setDate(newDate.getDate() - 1)
|
| 143 |
+
}
|
| 144 |
+
setCurrentDate(newDate)
|
| 145 |
}
|
| 146 |
|
| 147 |
+
const handleNext = () => {
|
| 148 |
+
const newDate = new Date(currentDate)
|
| 149 |
+
if (view === 'month') {
|
| 150 |
+
newDate.setMonth(newDate.getMonth() + 1)
|
| 151 |
+
} else if (view === 'year') {
|
| 152 |
+
newDate.setFullYear(newDate.getFullYear() + 1)
|
| 153 |
+
} else if (view === 'week') {
|
| 154 |
+
newDate.setDate(newDate.getDate() + 7)
|
| 155 |
+
} else if (view === 'day') {
|
| 156 |
+
newDate.setDate(newDate.getDate() + 1)
|
| 157 |
+
}
|
| 158 |
+
setCurrentDate(newDate)
|
| 159 |
}
|
| 160 |
|
| 161 |
+
const handleToday = () => {
|
| 162 |
+
setCurrentDate(new Date())
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
+
// Helper for Week View
|
| 166 |
+
const getWeekDays = () => {
|
| 167 |
+
const startOfWeek = new Date(currentDate)
|
| 168 |
+
const day = startOfWeek.getDay()
|
| 169 |
+
const diff = startOfWeek.getDate() - day
|
| 170 |
+
startOfWeek.setDate(diff)
|
| 171 |
+
|
| 172 |
+
const days = []
|
| 173 |
+
for (let i = 0; i < 7; i++) {
|
| 174 |
+
const d = new Date(startOfWeek)
|
| 175 |
+
d.setDate(startOfWeek.getDate() + i)
|
| 176 |
+
days.push(d)
|
| 177 |
}
|
| 178 |
+
return days
|
| 179 |
}
|
| 180 |
|
| 181 |
+
const renderHeaderTitle = () => {
|
| 182 |
+
if (view === 'year') {
|
| 183 |
+
return <span className="font-bold">{currentDate.getFullYear()}</span>
|
| 184 |
+
}
|
| 185 |
+
if (view === 'day') {
|
| 186 |
+
return (
|
| 187 |
+
<div className="flex flex-col leading-tight">
|
| 188 |
+
<span className="font-bold text-2xl">{currentDate.getDate()} {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
|
| 189 |
+
<span className="text-lg font-normal text-gray-400">{weekDays[currentDate.getDay()]}</span>
|
| 190 |
+
</div>
|
| 191 |
+
)
|
| 192 |
+
}
|
| 193 |
+
return (
|
| 194 |
+
<>
|
| 195 |
+
<span className="font-bold">{monthNames[currentDate.getMonth()]}</span> {currentDate.getFullYear()}
|
| 196 |
+
</>
|
| 197 |
+
)
|
| 198 |
+
}
|
| 199 |
|
| 200 |
return (
|
| 201 |
<Window
|
|
|
|
| 205 |
onClose={onClose}
|
| 206 |
onMinimize={onMinimize}
|
| 207 |
onMaximize={onMaximize}
|
| 208 |
+
width={1100}
|
| 209 |
+
height={750}
|
| 210 |
+
x={50}
|
| 211 |
+
y={50}
|
| 212 |
className="calendar-window"
|
| 213 |
>
|
| 214 |
+
<div className="flex flex-col h-full bg-[#1E1E1E] text-white overflow-hidden">
|
| 215 |
+
{/* Toolbar */}
|
| 216 |
+
<div className="h-14 flex items-center justify-between px-4 border-b border-[#333] bg-[#252526]">
|
| 217 |
+
<div className="flex items-center gap-4">
|
| 218 |
+
<div className="flex items-center gap-2">
|
| 219 |
+
<button
|
| 220 |
+
onClick={handleToday}
|
| 221 |
+
className="p-1.5 hover:bg-[#333] rounded-md text-gray-400 hover:text-white transition-colors"
|
| 222 |
+
title="Go to Today"
|
| 223 |
+
>
|
| 224 |
+
<CalendarBlank size={18} />
|
| 225 |
+
</button>
|
| 226 |
+
</div>
|
| 227 |
+
<div className="h-6 w-px bg-[#444]" />
|
| 228 |
+
<button className="p-1.5 hover:bg-[#333] rounded-full text-gray-400 hover:text-white transition-colors">
|
| 229 |
+
<Plus size={18} weight="bold" />
|
| 230 |
+
</button>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div className="flex items-center bg-[#111] rounded-lg p-1 border border-[#333]">
|
| 234 |
+
{(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
|
| 235 |
+
<button
|
| 236 |
+
key={v}
|
| 237 |
+
onClick={() => setView(v)}
|
| 238 |
+
className={`px-3 py-1 text-xs font-medium rounded-md capitalize transition-all ${view === v
|
| 239 |
+
? 'bg-[#333] text-white shadow-sm'
|
| 240 |
+
: 'text-gray-400 hover:text-gray-200'
|
| 241 |
+
}`}
|
| 242 |
+
>
|
| 243 |
+
{v}
|
| 244 |
+
</button>
|
| 245 |
+
))}
|
| 246 |
+
</div>
|
| 247 |
+
|
| 248 |
+
<div className="flex items-center gap-2">
|
| 249 |
+
<div className={`flex items-center bg-[#111] border border-[#333] rounded-md px-2 py-1 transition-all ${isSearchOpen ? 'w-48' : 'w-8 border-transparent bg-transparent'}`}>
|
| 250 |
+
<button onClick={() => setIsSearchOpen(!isSearchOpen)} className="text-gray-400 hover:text-white">
|
| 251 |
+
<MagnifyingGlass size={16} />
|
| 252 |
+
</button>
|
| 253 |
+
{isSearchOpen && (
|
| 254 |
+
<input
|
| 255 |
+
type="text"
|
| 256 |
+
placeholder="Search"
|
| 257 |
+
className="bg-transparent border-none outline-none text-xs text-white ml-2 w-full"
|
| 258 |
+
value={searchQuery}
|
| 259 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 260 |
+
autoFocus
|
| 261 |
+
/>
|
| 262 |
+
)}
|
| 263 |
+
</div>
|
| 264 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
</div>
|
| 266 |
|
| 267 |
+
{/* Header */}
|
| 268 |
+
<div className="h-20 flex items-center justify-between px-6 py-4 shrink-0">
|
| 269 |
+
<h1 className="text-3xl font-light tracking-tight flex items-baseline gap-2">
|
| 270 |
+
{renderHeaderTitle()}
|
| 271 |
+
</h1>
|
| 272 |
+
|
| 273 |
+
<div className="flex items-center gap-2">
|
| 274 |
+
<button
|
| 275 |
+
onClick={handlePrev}
|
| 276 |
+
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[#333] transition-colors"
|
| 277 |
>
|
| 278 |
+
<CaretLeft size={20} />
|
| 279 |
+
</button>
|
| 280 |
+
<button
|
| 281 |
+
onClick={handleToday}
|
| 282 |
+
className="px-3 py-1 bg-[#333] hover:bg-[#444] rounded-md text-sm font-medium transition-colors border border-[#444]"
|
| 283 |
+
>
|
| 284 |
+
Today
|
| 285 |
+
</button>
|
| 286 |
+
<button
|
| 287 |
+
onClick={handleNext}
|
| 288 |
+
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[#333] transition-colors"
|
| 289 |
+
>
|
| 290 |
+
<CaretRight size={20} />
|
| 291 |
+
</button>
|
| 292 |
+
</div>
|
| 293 |
</div>
|
| 294 |
|
| 295 |
+
{/* Views */}
|
| 296 |
+
<div className="flex-1 overflow-hidden flex flex-col relative">
|
| 297 |
+
|
| 298 |
+
{/* YEAR VIEW */}
|
| 299 |
+
{view === 'year' && (
|
| 300 |
+
<div className="flex-1 overflow-y-auto p-6">
|
| 301 |
+
<div className="grid grid-cols-4 gap-x-8 gap-y-8">
|
| 302 |
+
{monthNames.map((month, monthIndex) => {
|
| 303 |
+
const days = getDaysInMonth(currentDate.getFullYear(), monthIndex)
|
| 304 |
+
return (
|
| 305 |
+
<div key={month} className="flex flex-col gap-2">
|
| 306 |
+
<h3 className="text-red-500 font-medium text-lg pl-1">{month}</h3>
|
| 307 |
+
<div className="grid grid-cols-7 gap-y-2 text-center">
|
| 308 |
+
{/* Weekday Headers */}
|
| 309 |
+
{weekDays.map(d => (
|
| 310 |
+
<div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
|
| 311 |
+
))}
|
| 312 |
+
{/* Days */}
|
| 313 |
+
{days.map((d, i) => {
|
| 314 |
+
if (!d.isCurrentMonth) return <div key={i} />
|
| 315 |
+
const dateStr = `${d.year}-${String(d.month + 1).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`
|
| 316 |
+
const hasEvents = getEventsForDate(dateStr).length > 0
|
| 317 |
+
const isCurrentDay = isToday(d.day, d.month, d.year)
|
| 318 |
+
|
| 319 |
+
return (
|
| 320 |
+
<div
|
| 321 |
+
key={i}
|
| 322 |
+
className={`
|
| 323 |
+
text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer hover:bg-[#333] relative
|
| 324 |
+
${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-gray-300'}
|
| 325 |
+
`}
|
| 326 |
+
onClick={() => {
|
| 327 |
+
setCurrentDate(new Date(d.year, d.month, d.day))
|
| 328 |
+
setView('day')
|
| 329 |
+
}}
|
| 330 |
+
>
|
| 331 |
+
{d.day}
|
| 332 |
+
{hasEvents && !isCurrentDay && (
|
| 333 |
+
<div className="absolute bottom-0.5 w-0.5 h-0.5 bg-gray-400 rounded-full" />
|
| 334 |
+
)}
|
| 335 |
+
</div>
|
| 336 |
+
)
|
| 337 |
+
})}
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
)
|
| 341 |
+
})}
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
)}
|
| 345 |
+
|
| 346 |
+
{/* MONTH VIEW */}
|
| 347 |
+
{view === 'month' && (
|
| 348 |
+
<div className="flex-1 flex flex-col px-4 pb-4 overflow-hidden">
|
| 349 |
+
<div className="grid grid-cols-7 mb-2 shrink-0">
|
| 350 |
+
{weekDays.map(day => (
|
| 351 |
+
<div key={day} className="text-right pr-2 text-sm font-medium text-gray-500">
|
| 352 |
+
{day}
|
| 353 |
</div>
|
| 354 |
+
))}
|
| 355 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
+
<div className="flex-1 grid grid-cols-7 grid-rows-6 border-t border-l border-[#333] bg-[#1E1E1E]">
|
| 358 |
+
{calendarDays.map((dateObj, index) => {
|
| 359 |
+
const dateStr = `${dateObj.year}-${String(dateObj.month + 1).padStart(2, '0')}-${String(dateObj.day).padStart(2, '0')}`
|
| 360 |
+
const dayEvents = getEventsForDate(dateStr)
|
| 361 |
+
const isCurrentDay = isToday(dateObj.day, dateObj.month, dateObj.year)
|
| 362 |
+
|
| 363 |
+
return (
|
| 364 |
+
<div
|
| 365 |
+
key={index}
|
| 366 |
+
className={`
|
| 367 |
+
border-b border-r border-[#333] p-1 relative group transition-colors
|
| 368 |
+
${!dateObj.isCurrentMonth ? 'bg-[#1a1a1a] text-gray-600' : 'hover:bg-[#252526]'}
|
| 369 |
+
`}
|
| 370 |
+
onClick={() => {
|
| 371 |
+
setCurrentDate(new Date(dateObj.year, dateObj.month, dateObj.day))
|
| 372 |
+
setView('day')
|
| 373 |
+
}}
|
| 374 |
+
>
|
| 375 |
+
<div className="flex justify-end mb-1">
|
| 376 |
+
<span
|
| 377 |
+
className={`
|
| 378 |
+
text-sm font-medium w-7 h-7 flex items-center justify-center rounded-full
|
| 379 |
+
${isCurrentDay
|
| 380 |
+
? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
|
| 381 |
+
: dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
|
| 382 |
+
`}
|
| 383 |
+
>
|
| 384 |
+
{dateObj.day}
|
| 385 |
+
</span>
|
| 386 |
+
</div>
|
| 387 |
+
|
| 388 |
+
<div className="space-y-1 overflow-y-auto max-h-[calc(100%-2rem)] custom-scrollbar">
|
| 389 |
+
{dayEvents.map((event, i) => (
|
| 390 |
+
<div
|
| 391 |
+
key={i}
|
| 392 |
+
className={`
|
| 393 |
+
text-[10px] px-1.5 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
|
| 394 |
+
${event.color || 'bg-blue-500'} text-white font-medium shadow-sm
|
| 395 |
+
${event.id === 'reuben-bday' ? 'animate-pulse ring-1 ring-pink-300' : ''}
|
| 396 |
+
`}
|
| 397 |
+
title={event.title}
|
| 398 |
+
>
|
| 399 |
+
{event.title}
|
| 400 |
+
</div>
|
| 401 |
+
))}
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
)
|
| 405 |
+
})}
|
| 406 |
+
</div>
|
| 407 |
</div>
|
| 408 |
+
)}
|
| 409 |
+
|
| 410 |
+
{/* WEEK VIEW */}
|
| 411 |
+
{view === 'week' && (
|
| 412 |
+
<div className="flex-1 flex flex-col overflow-hidden">
|
| 413 |
+
{/* Week Header */}
|
| 414 |
+
<div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 pb-2">
|
| 415 |
+
{getWeekDays().map((d, i) => {
|
| 416 |
+
const isCurrentDay = isToday(d.getDate(), d.getMonth(), d.getFullYear())
|
| 417 |
+
return (
|
| 418 |
+
<div key={i} className="flex-1 text-center border-l border-[#333] first:border-l-0">
|
| 419 |
+
<div className={`text-xs font-medium mb-1 ${isCurrentDay ? 'text-red-500' : 'text-gray-500'}`}>
|
| 420 |
+
{weekDays[d.getDay()]}
|
| 421 |
+
</div>
|
| 422 |
+
<div className={`
|
| 423 |
+
text-xl font-light w-8 h-8 flex items-center justify-center rounded-full mx-auto
|
| 424 |
+
${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-white'}
|
| 425 |
+
`}>
|
| 426 |
+
{d.getDate()}
|
| 427 |
+
</div>
|
| 428 |
</div>
|
| 429 |
+
)
|
| 430 |
+
})}
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
{/* All Day Section */}
|
| 434 |
+
<div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 py-1 min-h-[40px]">
|
| 435 |
+
<div className="absolute left-2 text-[10px] text-gray-500 top-32">all-day</div>
|
| 436 |
+
{getWeekDays().map((d, i) => {
|
| 437 |
+
const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
| 438 |
+
const dayEvents = getEventsForDate(dateStr)
|
| 439 |
+
return (
|
| 440 |
+
<div key={i} className="flex-1 px-1 border-l border-[#333] first:border-l-0 space-y-1">
|
| 441 |
+
{dayEvents.map((event, idx) => (
|
| 442 |
+
<div
|
| 443 |
+
key={idx}
|
| 444 |
+
className={`
|
| 445 |
+
text-[10px] px-1.5 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
|
| 446 |
+
${event.color || 'bg-blue-500'} text-white font-medium
|
| 447 |
+
`}
|
| 448 |
+
title={event.title}
|
| 449 |
+
>
|
| 450 |
+
{event.title}
|
| 451 |
+
</div>
|
| 452 |
+
))}
|
| 453 |
</div>
|
| 454 |
+
)
|
| 455 |
+
})}
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
{/* Scrollable Time Grid */}
|
| 459 |
+
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
| 460 |
+
<div className="absolute top-0 left-0 w-full min-h-full">
|
| 461 |
+
{hours.map((hour) => (
|
| 462 |
+
<div key={hour} className="flex h-[60px] border-b border-[#333] group">
|
| 463 |
+
{/* Time Label */}
|
| 464 |
+
<div className="w-14 shrink-0 text-right pr-2 text-xs text-gray-500 -mt-2.5 bg-[#1E1E1E] z-10">
|
| 465 |
+
{hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
|
| 466 |
+
</div>
|
| 467 |
+
{/* Columns */}
|
| 468 |
+
<div className="flex-1 flex">
|
| 469 |
+
{Array.from({ length: 7 }).map((_, colIndex) => (
|
| 470 |
+
<div key={colIndex} className="flex-1 border-l border-[#333] first:border-l-0 relative group-hover:bg-[#252526]/30 transition-colors">
|
| 471 |
+
{/* Time slots would go here */}
|
| 472 |
+
</div>
|
| 473 |
+
))}
|
| 474 |
+
</div>
|
| 475 |
+
</div>
|
| 476 |
+
))}
|
| 477 |
+
{/* Current Time Indicator (if in current week) */}
|
| 478 |
+
{/* Simplified: just show if today is in view */}
|
| 479 |
</div>
|
| 480 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
</div>
|
| 482 |
+
)}
|
| 483 |
+
|
| 484 |
+
{/* DAY VIEW */}
|
| 485 |
+
{view === 'day' && (
|
| 486 |
+
<div className="flex-1 flex flex-col overflow-hidden">
|
| 487 |
+
{/* All Day Section */}
|
| 488 |
+
<div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 py-2 min-h-[50px]">
|
| 489 |
+
<div className="w-14 shrink-0 text-[10px] text-gray-500 pt-1 pr-2 text-right">all-day</div>
|
| 490 |
+
<div className="flex-1 px-1 space-y-1 border-l border-[#333]">
|
| 491 |
+
{getEventsForDate(`${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`).map((event, idx) => (
|
| 492 |
<div
|
| 493 |
+
key={idx}
|
| 494 |
+
className={`
|
| 495 |
+
text-xs px-2 py-1 rounded-sm truncate cursor-pointer hover:opacity-80
|
| 496 |
+
${event.color || 'bg-blue-500'} text-white font-medium
|
| 497 |
+
`}
|
| 498 |
+
title={event.title}
|
| 499 |
+
>
|
| 500 |
+
{event.title}
|
| 501 |
+
</div>
|
| 502 |
+
))}
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
{/* Scrollable Time Grid */}
|
| 507 |
+
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
|
| 508 |
+
<div className="absolute top-0 left-0 w-full min-h-full">
|
| 509 |
+
{hours.map((hour) => (
|
| 510 |
+
<div key={hour} className="flex h-[60px] border-b border-[#333]">
|
| 511 |
+
{/* Time Label */}
|
| 512 |
+
<div className="w-14 shrink-0 text-right pr-2 text-xs text-gray-500 -mt-2.5 bg-[#1E1E1E] z-10">
|
| 513 |
+
{hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
|
| 514 |
+
</div>
|
| 515 |
+
{/* Column */}
|
| 516 |
+
<div className="flex-1 border-l border-[#333] relative hover:bg-[#252526]/30 transition-colors">
|
| 517 |
+
{/* Time slots would go here */}
|
| 518 |
</div>
|
| 519 |
</div>
|
| 520 |
+
))}
|
| 521 |
+
</div>
|
| 522 |
</div>
|
| 523 |
+
</div>
|
| 524 |
+
)}
|
| 525 |
+
|
| 526 |
+
</div>
|
| 527 |
</div>
|
| 528 |
</Window>
|
| 529 |
)
|
app/components/Desktop.tsx
CHANGED
|
@@ -24,6 +24,18 @@ import { FlutterCodeEditor } from './FlutterCodeEditor'
|
|
| 24 |
import { LaTeXEditor } from './LaTeXEditor'
|
| 25 |
import { motion, AnimatePresence } from 'framer-motion'
|
| 26 |
import { SystemPowerOverlay } from './SystemPowerOverlay'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
export function Desktop() {
|
| 29 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
|
@@ -366,6 +378,153 @@ export function Desktop() {
|
|
| 366 |
}
|
| 367 |
}
|
| 368 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
return (
|
| 370 |
<div className="relative h-screen w-screen overflow-hidden flex touch-auto" onContextMenu={handleDesktopRightClick}>
|
| 371 |
<div
|
|
@@ -411,6 +570,7 @@ export function Desktop() {
|
|
| 411 |
browser: browserOpen,
|
| 412 |
gemini: geminiChatOpen
|
| 413 |
}}
|
|
|
|
| 414 |
/>
|
| 415 |
|
| 416 |
<div className="flex-1 z-10 relative">
|
|
@@ -532,16 +692,17 @@ export function Desktop() {
|
|
| 532 |
{fileManagerOpen && (
|
| 533 |
<motion.div
|
| 534 |
key="file-manager"
|
| 535 |
-
initial={{ scale: 0.
|
| 536 |
animate={{
|
| 537 |
-
scale: fileManagerMinimized ? 0 : 1,
|
| 538 |
opacity: fileManagerMinimized ? 0 : 1,
|
| 539 |
-
y: fileManagerMinimized ?
|
|
|
|
| 540 |
}}
|
| 541 |
-
exit={{ scale: 0.
|
| 542 |
-
transition={{ duration: 0.
|
| 543 |
className="absolute inset-0"
|
| 544 |
-
style={{ pointerEvents: fileManagerMinimized ? 'none' : 'auto' }}
|
| 545 |
>
|
| 546 |
<FileManager
|
| 547 |
currentPath={currentPath}
|
|
@@ -549,6 +710,7 @@ export function Desktop() {
|
|
| 549 |
onClose={closeFileManager}
|
| 550 |
onOpenFlutterApp={openFlutterRunner}
|
| 551 |
onMinimize={() => setFileManagerMinimized(true)}
|
|
|
|
| 552 |
/>
|
| 553 |
</motion.div>
|
| 554 |
)}
|
|
@@ -556,16 +718,17 @@ export function Desktop() {
|
|
| 556 |
{calendarOpen && (
|
| 557 |
<motion.div
|
| 558 |
key="calendar"
|
| 559 |
-
initial={{ scale: 0.
|
| 560 |
animate={{
|
| 561 |
-
scale: calendarMinimized ? 0 : 1,
|
| 562 |
opacity: calendarMinimized ? 0 : 1,
|
| 563 |
-
y: calendarMinimized ?
|
|
|
|
| 564 |
}}
|
| 565 |
-
exit={{ scale: 0.
|
| 566 |
-
transition={{ duration: 0.
|
| 567 |
className="absolute inset-0"
|
| 568 |
-
style={{ pointerEvents: calendarMinimized ? 'none' : 'auto' }}
|
| 569 |
>
|
| 570 |
<Calendar onClose={closeCalendar} onMinimize={() => setCalendarMinimized(true)} />
|
| 571 |
</motion.div>
|
|
@@ -574,16 +737,17 @@ export function Desktop() {
|
|
| 574 |
{clockOpen && (
|
| 575 |
<motion.div
|
| 576 |
key="clock"
|
| 577 |
-
initial={{ scale: 0.
|
| 578 |
animate={{
|
| 579 |
-
scale: clockMinimized ? 0 : 1,
|
| 580 |
opacity: clockMinimized ? 0 : 1,
|
| 581 |
-
y: clockMinimized ?
|
|
|
|
| 582 |
}}
|
| 583 |
-
exit={{ scale: 0.
|
| 584 |
-
transition={{ duration: 0.
|
| 585 |
className="absolute inset-0"
|
| 586 |
-
style={{ pointerEvents: clockMinimized ? 'none' : 'auto' }}
|
| 587 |
>
|
| 588 |
<Clock onClose={closeClock} onMinimize={() => setClockMinimized(true)} />
|
| 589 |
</motion.div>
|
|
@@ -592,16 +756,17 @@ export function Desktop() {
|
|
| 592 |
{browserOpen && (
|
| 593 |
<motion.div
|
| 594 |
key="browser"
|
| 595 |
-
initial={{ scale: 0.
|
| 596 |
animate={{
|
| 597 |
-
scale: browserMinimized ? 0 : 1,
|
| 598 |
opacity: browserMinimized ? 0 : 1,
|
| 599 |
-
y: browserMinimized ?
|
|
|
|
| 600 |
}}
|
| 601 |
-
exit={{ scale: 0.
|
| 602 |
-
transition={{ duration: 0.
|
| 603 |
className="absolute inset-0"
|
| 604 |
-
style={{ pointerEvents: browserMinimized ? 'none' : 'auto' }}
|
| 605 |
>
|
| 606 |
<WebBrowserApp onClose={closeBrowser} onMinimize={() => setBrowserMinimized(true)} />
|
| 607 |
</motion.div>
|
|
@@ -610,16 +775,17 @@ export function Desktop() {
|
|
| 610 |
{terminalOpen && (
|
| 611 |
<motion.div
|
| 612 |
key="terminal"
|
| 613 |
-
initial={{ scale: 0.
|
| 614 |
animate={{
|
| 615 |
-
scale: terminalMinimized ? 0 : 1,
|
| 616 |
opacity: terminalMinimized ? 0 : 1,
|
| 617 |
-
y: terminalMinimized ?
|
|
|
|
| 618 |
}}
|
| 619 |
-
exit={{ scale: 0.
|
| 620 |
-
transition={{ duration: 0.
|
| 621 |
className="absolute inset-0"
|
| 622 |
-
style={{ pointerEvents: terminalMinimized ? 'none' : 'auto' }}
|
| 623 |
>
|
| 624 |
<Terminal onClose={closeTerminal} onMinimize={() => setTerminalMinimized(true)} />
|
| 625 |
</motion.div>
|
|
@@ -628,16 +794,17 @@ export function Desktop() {
|
|
| 628 |
{sessionManagerOpen && sessionInitialized && (
|
| 629 |
<motion.div
|
| 630 |
key="session-manager"
|
| 631 |
-
initial={{ scale: 0.
|
| 632 |
animate={{
|
| 633 |
-
scale: sessionManagerMinimized ? 0 : 1,
|
| 634 |
opacity: sessionManagerMinimized ? 0 : 1,
|
| 635 |
-
y: sessionManagerMinimized ?
|
|
|
|
| 636 |
}}
|
| 637 |
-
exit={{ scale: 0.
|
| 638 |
-
transition={{ duration: 0.
|
| 639 |
className="absolute inset-0"
|
| 640 |
-
style={{ pointerEvents: sessionManagerMinimized ? 'none' : 'auto' }}
|
| 641 |
>
|
| 642 |
<SessionManagerWindow
|
| 643 |
onClose={closeSessionManager}
|
|
@@ -651,16 +818,17 @@ export function Desktop() {
|
|
| 651 |
{flutterRunnerOpen && activeFlutterApp && (
|
| 652 |
<motion.div
|
| 653 |
key="flutter-runner"
|
| 654 |
-
initial={{ scale: 0.
|
| 655 |
animate={{
|
| 656 |
-
scale: flutterRunnerMinimized ? 0 : 1,
|
| 657 |
opacity: flutterRunnerMinimized ? 0 : 1,
|
| 658 |
-
y: flutterRunnerMinimized ?
|
|
|
|
| 659 |
}}
|
| 660 |
-
exit={{ scale: 0.
|
| 661 |
-
transition={{ duration: 0.
|
| 662 |
className="absolute inset-0"
|
| 663 |
-
style={{ pointerEvents: flutterRunnerMinimized ? 'none' : 'auto' }}
|
| 664 |
>
|
| 665 |
<FlutterRunner
|
| 666 |
file={activeFlutterApp}
|
|
@@ -673,16 +841,17 @@ export function Desktop() {
|
|
| 673 |
{researchBrowserOpen && (
|
| 674 |
<motion.div
|
| 675 |
key="research-browser"
|
| 676 |
-
initial={{ scale: 0.
|
| 677 |
animate={{
|
| 678 |
-
scale: researchBrowserMinimized ? 0 : 1,
|
| 679 |
opacity: researchBrowserMinimized ? 0 : 1,
|
| 680 |
-
y: researchBrowserMinimized ?
|
|
|
|
| 681 |
}}
|
| 682 |
-
exit={{ scale: 0.
|
| 683 |
-
transition={{ duration: 0.
|
| 684 |
className="absolute inset-0"
|
| 685 |
-
style={{ pointerEvents: researchBrowserMinimized ? 'none' : 'auto' }}
|
| 686 |
>
|
| 687 |
<ResearchBrowser
|
| 688 |
onClose={closeResearchBrowser}
|
|
@@ -694,16 +863,17 @@ export function Desktop() {
|
|
| 694 |
{flutterCodeEditorOpen && (
|
| 695 |
<motion.div
|
| 696 |
key="flutter-code-editor"
|
| 697 |
-
initial={{ scale: 0.
|
| 698 |
animate={{
|
| 699 |
-
scale: flutterCodeEditorMinimized ? 0 : 1,
|
| 700 |
opacity: flutterCodeEditorMinimized ? 0 : 1,
|
| 701 |
-
y: flutterCodeEditorMinimized ?
|
|
|
|
| 702 |
}}
|
| 703 |
-
exit={{ scale: 0.
|
| 704 |
-
transition={{ duration: 0.
|
| 705 |
className="absolute inset-0"
|
| 706 |
-
style={{ pointerEvents: flutterCodeEditorMinimized ? 'none' : 'auto' }}
|
| 707 |
>
|
| 708 |
<FlutterCodeEditor onClose={closeFlutterCodeEditor} onMinimize={() => setFlutterCodeEditorMinimized(true)} />
|
| 709 |
</motion.div>
|
|
@@ -712,16 +882,17 @@ export function Desktop() {
|
|
| 712 |
{latexEditorOpen && (
|
| 713 |
<motion.div
|
| 714 |
key="latex-editor"
|
| 715 |
-
initial={{ scale: 0.
|
| 716 |
animate={{
|
| 717 |
-
scale: latexEditorMinimized ? 0 : 1,
|
| 718 |
opacity: latexEditorMinimized ? 0 : 1,
|
| 719 |
-
y: latexEditorMinimized ?
|
|
|
|
| 720 |
}}
|
| 721 |
-
exit={{ scale: 0.
|
| 722 |
-
transition={{ duration: 0.
|
| 723 |
className="absolute inset-0"
|
| 724 |
-
style={{ pointerEvents: latexEditorMinimized ? 'none' : 'auto' }}
|
| 725 |
>
|
| 726 |
<LaTeXEditor onClose={closeLaTeXEditor} sessionId={userSession} onMinimize={() => setLaTeXEditorMinimized(true)} />
|
| 727 |
</motion.div>
|
|
@@ -729,16 +900,17 @@ export function Desktop() {
|
|
| 729 |
{geminiChatOpen && (
|
| 730 |
<motion.div
|
| 731 |
key="gemini"
|
| 732 |
-
initial={{ scale: 0.
|
| 733 |
animate={{
|
| 734 |
-
scale: geminiChatMinimized ? 0 : 1,
|
| 735 |
opacity: geminiChatMinimized ? 0 : 1,
|
| 736 |
-
y: geminiChatMinimized ?
|
|
|
|
| 737 |
}}
|
| 738 |
-
exit={{ scale: 0.
|
| 739 |
-
transition={{ duration: 0.
|
| 740 |
className="absolute inset-0"
|
| 741 |
-
style={{ pointerEvents: geminiChatMinimized ? 'none' : 'auto' }}
|
| 742 |
>
|
| 743 |
<GeminiChat onClose={closeGeminiChat} onMinimize={() => setGeminiChatMinimized(true)} />
|
| 744 |
</motion.div>
|
|
|
|
| 24 |
import { LaTeXEditor } from './LaTeXEditor'
|
| 25 |
import { motion, AnimatePresence } from 'framer-motion'
|
| 26 |
import { SystemPowerOverlay } from './SystemPowerOverlay'
|
| 27 |
+
import {
|
| 28 |
+
Folder,
|
| 29 |
+
Calendar as CalendarIcon,
|
| 30 |
+
Clock as ClockIcon,
|
| 31 |
+
Globe,
|
| 32 |
+
Sparkle,
|
| 33 |
+
Terminal as TerminalIcon,
|
| 34 |
+
Key,
|
| 35 |
+
Brain,
|
| 36 |
+
Code,
|
| 37 |
+
FileText
|
| 38 |
+
} from '@phosphor-icons/react'
|
| 39 |
|
| 40 |
export function Desktop() {
|
| 41 |
const [fileManagerOpen, setFileManagerOpen] = useState(true)
|
|
|
|
| 378 |
}
|
| 379 |
}
|
| 380 |
|
| 381 |
+
// Build minimized apps list for dock
|
| 382 |
+
const minimizedApps = []
|
| 383 |
+
|
| 384 |
+
if (fileManagerMinimized && fileManagerOpen) {
|
| 385 |
+
minimizedApps.push({
|
| 386 |
+
id: 'files',
|
| 387 |
+
label: 'Files',
|
| 388 |
+
icon: (
|
| 389 |
+
<div className="bg-gradient-to-br from-blue-400 to-cyan-200 w-full h-full rounded-xl flex items-center justify-center border border-white/30">
|
| 390 |
+
<Folder size={20} weight="regular" className="text-blue-900" />
|
| 391 |
+
</div>
|
| 392 |
+
),
|
| 393 |
+
onRestore: () => setFileManagerMinimized(false)
|
| 394 |
+
})
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
if (calendarMinimized && calendarOpen) {
|
| 398 |
+
minimizedApps.push({
|
| 399 |
+
id: 'calendar',
|
| 400 |
+
label: 'Calendar',
|
| 401 |
+
icon: (
|
| 402 |
+
<div className="bg-gradient-to-br from-purple-500 to-purple-600 w-full h-full rounded-xl flex items-center justify-center shadow-inner">
|
| 403 |
+
<CalendarIcon size={20} weight="regular" className="text-white" />
|
| 404 |
+
</div>
|
| 405 |
+
),
|
| 406 |
+
onRestore: () => setCalendarMinimized(false)
|
| 407 |
+
})
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
if (clockMinimized && clockOpen) {
|
| 411 |
+
minimizedApps.push({
|
| 412 |
+
id: 'clock',
|
| 413 |
+
label: 'Clock',
|
| 414 |
+
icon: (
|
| 415 |
+
<div className="bg-black w-full h-full rounded-full flex items-center justify-center border border-gray-600">
|
| 416 |
+
<ClockIcon size={20} weight="regular" className="text-white" />
|
| 417 |
+
</div>
|
| 418 |
+
),
|
| 419 |
+
onRestore: () => setClockMinimized(false)
|
| 420 |
+
})
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
if (browserMinimized && browserOpen) {
|
| 424 |
+
minimizedApps.push({
|
| 425 |
+
id: 'browser',
|
| 426 |
+
label: 'Browser',
|
| 427 |
+
icon: (
|
| 428 |
+
<div className="bg-white w-full h-full rounded-xl overflow-hidden relative flex items-center justify-center">
|
| 429 |
+
<div className="absolute inset-0 bg-gradient-to-tr from-blue-500 to-cyan-300" />
|
| 430 |
+
<Globe size={20} weight="light" className="text-white relative z-10" />
|
| 431 |
+
</div>
|
| 432 |
+
),
|
| 433 |
+
onRestore: () => setBrowserMinimized(false)
|
| 434 |
+
})
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
if (geminiChatMinimized && geminiChatOpen) {
|
| 438 |
+
minimizedApps.push({
|
| 439 |
+
id: 'gemini',
|
| 440 |
+
label: 'Gemini',
|
| 441 |
+
icon: (
|
| 442 |
+
<div className="bg-white w-full h-full rounded-xl flex items-center justify-center border border-gray-200">
|
| 443 |
+
<Sparkle size={20} weight="fill" className="text-blue-500" />
|
| 444 |
+
</div>
|
| 445 |
+
),
|
| 446 |
+
onRestore: () => setGeminiChatMinimized(false)
|
| 447 |
+
})
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
if (terminalMinimized && terminalOpen) {
|
| 451 |
+
minimizedApps.push({
|
| 452 |
+
id: 'terminal',
|
| 453 |
+
label: 'Terminal',
|
| 454 |
+
icon: (
|
| 455 |
+
<div className="bg-black w-full h-full rounded-xl flex items-center justify-center border border-gray-700">
|
| 456 |
+
<TerminalIcon size={20} weight="bold" className="text-green-400" />
|
| 457 |
+
</div>
|
| 458 |
+
),
|
| 459 |
+
onRestore: () => setTerminalMinimized(false)
|
| 460 |
+
})
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
if (sessionManagerMinimized && sessionManagerOpen) {
|
| 464 |
+
minimizedApps.push({
|
| 465 |
+
id: 'sessions',
|
| 466 |
+
label: 'Sessions',
|
| 467 |
+
icon: (
|
| 468 |
+
<div className="bg-gradient-to-br from-indigo-500 to-blue-600 w-full h-full rounded-xl flex items-center justify-center">
|
| 469 |
+
<Key size={20} weight="bold" className="text-white" />
|
| 470 |
+
</div>
|
| 471 |
+
),
|
| 472 |
+
onRestore: () => setSessionManagerMinimized(false)
|
| 473 |
+
})
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
if (flutterRunnerMinimized && flutterRunnerOpen) {
|
| 477 |
+
minimizedApps.push({
|
| 478 |
+
id: 'flutter-runner',
|
| 479 |
+
label: 'Flutter Runner',
|
| 480 |
+
icon: (
|
| 481 |
+
<div className="bg-gradient-to-br from-cyan-400 to-blue-500 w-full h-full rounded-xl flex items-center justify-center">
|
| 482 |
+
<Code size={20} weight="bold" className="text-white" />
|
| 483 |
+
</div>
|
| 484 |
+
),
|
| 485 |
+
onRestore: () => setFlutterRunnerMinimized(false)
|
| 486 |
+
})
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
if (researchBrowserMinimized && researchBrowserOpen) {
|
| 490 |
+
minimizedApps.push({
|
| 491 |
+
id: 'research',
|
| 492 |
+
label: 'Research',
|
| 493 |
+
icon: (
|
| 494 |
+
<div className="bg-gradient-to-br from-purple-400 to-pink-500 w-full h-full rounded-xl flex items-center justify-center">
|
| 495 |
+
<Brain size={20} weight="bold" className="text-white" />
|
| 496 |
+
</div>
|
| 497 |
+
),
|
| 498 |
+
onRestore: () => setResearchBrowserMinimized(false)
|
| 499 |
+
})
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
if (flutterCodeEditorMinimized && flutterCodeEditorOpen) {
|
| 503 |
+
minimizedApps.push({
|
| 504 |
+
id: 'flutter-editor',
|
| 505 |
+
label: 'Flutter IDE',
|
| 506 |
+
icon: (
|
| 507 |
+
<div className="bg-gradient-to-br from-blue-500 to-cyan-500 w-full h-full rounded-xl flex items-center justify-center">
|
| 508 |
+
<Code size={20} weight="fill" className="text-white" />
|
| 509 |
+
</div>
|
| 510 |
+
),
|
| 511 |
+
onRestore: () => setFlutterCodeEditorMinimized(false)
|
| 512 |
+
})
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
if (latexEditorMinimized && latexEditorOpen) {
|
| 516 |
+
minimizedApps.push({
|
| 517 |
+
id: 'latex-editor',
|
| 518 |
+
label: 'LaTeX Studio',
|
| 519 |
+
icon: (
|
| 520 |
+
<div className="bg-black w-full h-full rounded-xl flex items-center justify-center">
|
| 521 |
+
<FileText size={20} weight="bold" className="text-white" />
|
| 522 |
+
</div>
|
| 523 |
+
),
|
| 524 |
+
onRestore: () => setLaTeXEditorMinimized(false)
|
| 525 |
+
})
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
return (
|
| 529 |
<div className="relative h-screen w-screen overflow-hidden flex touch-auto" onContextMenu={handleDesktopRightClick}>
|
| 530 |
<div
|
|
|
|
| 570 |
browser: browserOpen,
|
| 571 |
gemini: geminiChatOpen
|
| 572 |
}}
|
| 573 |
+
minimizedApps={minimizedApps}
|
| 574 |
/>
|
| 575 |
|
| 576 |
<div className="flex-1 z-10 relative">
|
|
|
|
| 692 |
{fileManagerOpen && (
|
| 693 |
<motion.div
|
| 694 |
key="file-manager"
|
| 695 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 696 |
animate={{
|
| 697 |
+
scale: fileManagerMinimized ? 0.1 : 1,
|
| 698 |
opacity: fileManagerMinimized ? 0 : 1,
|
| 699 |
+
y: fileManagerMinimized ? 400 : 0,
|
| 700 |
+
x: fileManagerMinimized ? '-40%' : 0
|
| 701 |
}}
|
| 702 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 703 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 704 |
className="absolute inset-0"
|
| 705 |
+
style={{ pointerEvents: fileManagerMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 706 |
>
|
| 707 |
<FileManager
|
| 708 |
currentPath={currentPath}
|
|
|
|
| 710 |
onClose={closeFileManager}
|
| 711 |
onOpenFlutterApp={openFlutterRunner}
|
| 712 |
onMinimize={() => setFileManagerMinimized(true)}
|
| 713 |
+
sessionId={userSession}
|
| 714 |
/>
|
| 715 |
</motion.div>
|
| 716 |
)}
|
|
|
|
| 718 |
{calendarOpen && (
|
| 719 |
<motion.div
|
| 720 |
key="calendar"
|
| 721 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 722 |
animate={{
|
| 723 |
+
scale: calendarMinimized ? 0.1 : 1,
|
| 724 |
opacity: calendarMinimized ? 0 : 1,
|
| 725 |
+
y: calendarMinimized ? 400 : 0,
|
| 726 |
+
x: calendarMinimized ? '20%' : 0
|
| 727 |
}}
|
| 728 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 729 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 730 |
className="absolute inset-0"
|
| 731 |
+
style={{ pointerEvents: calendarMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 732 |
>
|
| 733 |
<Calendar onClose={closeCalendar} onMinimize={() => setCalendarMinimized(true)} />
|
| 734 |
</motion.div>
|
|
|
|
| 737 |
{clockOpen && (
|
| 738 |
<motion.div
|
| 739 |
key="clock"
|
| 740 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 741 |
animate={{
|
| 742 |
+
scale: clockMinimized ? 0.1 : 1,
|
| 743 |
opacity: clockMinimized ? 0 : 1,
|
| 744 |
+
y: clockMinimized ? 400 : 0,
|
| 745 |
+
x: clockMinimized ? '10%' : 0
|
| 746 |
}}
|
| 747 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 748 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 749 |
className="absolute inset-0"
|
| 750 |
+
style={{ pointerEvents: clockMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 751 |
>
|
| 752 |
<Clock onClose={closeClock} onMinimize={() => setClockMinimized(true)} />
|
| 753 |
</motion.div>
|
|
|
|
| 756 |
{browserOpen && (
|
| 757 |
<motion.div
|
| 758 |
key="browser"
|
| 759 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 760 |
animate={{
|
| 761 |
+
scale: browserMinimized ? 0.1 : 1,
|
| 762 |
opacity: browserMinimized ? 0 : 1,
|
| 763 |
+
y: browserMinimized ? 400 : 0,
|
| 764 |
+
x: browserMinimized ? '-20%' : 0
|
| 765 |
}}
|
| 766 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 767 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 768 |
className="absolute inset-0"
|
| 769 |
+
style={{ pointerEvents: browserMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 770 |
>
|
| 771 |
<WebBrowserApp onClose={closeBrowser} onMinimize={() => setBrowserMinimized(true)} />
|
| 772 |
</motion.div>
|
|
|
|
| 775 |
{terminalOpen && (
|
| 776 |
<motion.div
|
| 777 |
key="terminal"
|
| 778 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 779 |
animate={{
|
| 780 |
+
scale: terminalMinimized ? 0.1 : 1,
|
| 781 |
opacity: terminalMinimized ? 0 : 1,
|
| 782 |
+
y: terminalMinimized ? 400 : 0,
|
| 783 |
+
x: terminalMinimized ? '30%' : 0
|
| 784 |
}}
|
| 785 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 786 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 787 |
className="absolute inset-0"
|
| 788 |
+
style={{ pointerEvents: terminalMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 789 |
>
|
| 790 |
<Terminal onClose={closeTerminal} onMinimize={() => setTerminalMinimized(true)} />
|
| 791 |
</motion.div>
|
|
|
|
| 794 |
{sessionManagerOpen && sessionInitialized && (
|
| 795 |
<motion.div
|
| 796 |
key="session-manager"
|
| 797 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 798 |
animate={{
|
| 799 |
+
scale: sessionManagerMinimized ? 0.1 : 1,
|
| 800 |
opacity: sessionManagerMinimized ? 0 : 1,
|
| 801 |
+
y: sessionManagerMinimized ? 400 : 0,
|
| 802 |
+
x: sessionManagerMinimized ? '40%' : 0
|
| 803 |
}}
|
| 804 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 805 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 806 |
className="absolute inset-0"
|
| 807 |
+
style={{ pointerEvents: sessionManagerMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 808 |
>
|
| 809 |
<SessionManagerWindow
|
| 810 |
onClose={closeSessionManager}
|
|
|
|
| 818 |
{flutterRunnerOpen && activeFlutterApp && (
|
| 819 |
<motion.div
|
| 820 |
key="flutter-runner"
|
| 821 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 822 |
animate={{
|
| 823 |
+
scale: flutterRunnerMinimized ? 0.1 : 1,
|
| 824 |
opacity: flutterRunnerMinimized ? 0 : 1,
|
| 825 |
+
y: flutterRunnerMinimized ? 400 : 0,
|
| 826 |
+
x: flutterRunnerMinimized ? '50%' : 0
|
| 827 |
}}
|
| 828 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 829 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 830 |
className="absolute inset-0"
|
| 831 |
+
style={{ pointerEvents: flutterRunnerMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 832 |
>
|
| 833 |
<FlutterRunner
|
| 834 |
file={activeFlutterApp}
|
|
|
|
| 841 |
{researchBrowserOpen && (
|
| 842 |
<motion.div
|
| 843 |
key="research-browser"
|
| 844 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 845 |
animate={{
|
| 846 |
+
scale: researchBrowserMinimized ? 0.1 : 1,
|
| 847 |
opacity: researchBrowserMinimized ? 0 : 1,
|
| 848 |
+
y: researchBrowserMinimized ? 400 : 0,
|
| 849 |
+
x: researchBrowserMinimized ? '-30%' : 0
|
| 850 |
}}
|
| 851 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 852 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 853 |
className="absolute inset-0"
|
| 854 |
+
style={{ pointerEvents: researchBrowserMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 855 |
>
|
| 856 |
<ResearchBrowser
|
| 857 |
onClose={closeResearchBrowser}
|
|
|
|
| 863 |
{flutterCodeEditorOpen && (
|
| 864 |
<motion.div
|
| 865 |
key="flutter-code-editor"
|
| 866 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 867 |
animate={{
|
| 868 |
+
scale: flutterCodeEditorMinimized ? 0.1 : 1,
|
| 869 |
opacity: flutterCodeEditorMinimized ? 0 : 1,
|
| 870 |
+
y: flutterCodeEditorMinimized ? 400 : 0,
|
| 871 |
+
x: flutterCodeEditorMinimized ? '50%' : 0
|
| 872 |
}}
|
| 873 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 874 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 875 |
className="absolute inset-0"
|
| 876 |
+
style={{ pointerEvents: flutterCodeEditorMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 877 |
>
|
| 878 |
<FlutterCodeEditor onClose={closeFlutterCodeEditor} onMinimize={() => setFlutterCodeEditorMinimized(true)} />
|
| 879 |
</motion.div>
|
|
|
|
| 882 |
{latexEditorOpen && (
|
| 883 |
<motion.div
|
| 884 |
key="latex-editor"
|
| 885 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 886 |
animate={{
|
| 887 |
+
scale: latexEditorMinimized ? 0.1 : 1,
|
| 888 |
opacity: latexEditorMinimized ? 0 : 1,
|
| 889 |
+
y: latexEditorMinimized ? 400 : 0,
|
| 890 |
+
x: latexEditorMinimized ? '60%' : 0
|
| 891 |
}}
|
| 892 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 893 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 894 |
className="absolute inset-0"
|
| 895 |
+
style={{ pointerEvents: latexEditorMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 896 |
>
|
| 897 |
<LaTeXEditor onClose={closeLaTeXEditor} sessionId={userSession} onMinimize={() => setLaTeXEditorMinimized(true)} />
|
| 898 |
</motion.div>
|
|
|
|
| 900 |
{geminiChatOpen && (
|
| 901 |
<motion.div
|
| 902 |
key="gemini"
|
| 903 |
+
initial={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 904 |
animate={{
|
| 905 |
+
scale: geminiChatMinimized ? 0.1 : 1,
|
| 906 |
opacity: geminiChatMinimized ? 0 : 1,
|
| 907 |
+
y: geminiChatMinimized ? 400 : 0,
|
| 908 |
+
x: geminiChatMinimized ? '0%' : 0
|
| 909 |
}}
|
| 910 |
+
exit={{ scale: 0.8, opacity: 0, y: 100 }}
|
| 911 |
+
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
|
| 912 |
className="absolute inset-0"
|
| 913 |
+
style={{ pointerEvents: geminiChatMinimized ? 'none' : 'auto', transformOrigin: 'bottom center' }}
|
| 914 |
>
|
| 915 |
<GeminiChat onClose={closeGeminiChat} onMinimize={() => setGeminiChatMinimized(true)} />
|
| 916 |
</motion.div>
|
app/components/Dock.tsx
CHANGED
|
@@ -12,6 +12,15 @@ import {
|
|
| 12 |
Compass
|
| 13 |
} from '@phosphor-icons/react'
|
| 14 |
import { motion } from 'framer-motion'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
interface DockProps {
|
| 17 |
onOpenFileManager: (path: string) => void
|
|
@@ -20,6 +29,7 @@ interface DockProps {
|
|
| 20 |
onOpenBrowser: () => void
|
| 21 |
onOpenGeminiChat: () => void
|
| 22 |
openApps: { [key: string]: boolean }
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
interface DockItemProps {
|
|
@@ -62,7 +72,8 @@ export function Dock({
|
|
| 62 |
onOpenClock,
|
| 63 |
onOpenBrowser,
|
| 64 |
onOpenGeminiChat,
|
| 65 |
-
openApps
|
|
|
|
| 66 |
}: DockProps) {
|
| 67 |
const mouseX = React.useRef<number | null>(null)
|
| 68 |
|
|
@@ -103,12 +114,8 @@ export function Dock({
|
|
| 103 |
},
|
| 104 |
{
|
| 105 |
icon: (
|
| 106 |
-
<div className="
|
| 107 |
-
<
|
| 108 |
-
<div className="absolute top-1/2 left-1/2 w-0.5 h-2.5 bg-white origin-bottom -translate-x-1/2 -translate-y-full" />
|
| 109 |
-
<div className="absolute top-1/2 left-1/2 w-0.5 h-3.5 bg-gray-300 origin-bottom -translate-x-1/2 -translate-y-full" />
|
| 110 |
-
<div className="absolute top-1/2 left-1/2 w-0.5 h-4 bg-orange-500 origin-bottom -translate-x-1/2 -translate-y-full animate-spin" style={{ animationDuration: '60s' }} />
|
| 111 |
-
</div>
|
| 112 |
</div>
|
| 113 |
),
|
| 114 |
label: 'Clock',
|
|
@@ -118,8 +125,8 @@ export function Dock({
|
|
| 118 |
},
|
| 119 |
{
|
| 120 |
icon: (
|
| 121 |
-
<div className="
|
| 122 |
-
<
|
| 123 |
</div>
|
| 124 |
),
|
| 125 |
label: 'Calendar',
|
|
@@ -137,26 +144,40 @@ export function Dock({
|
|
| 137 |
onMouseLeave={() => mouseX.current = null}
|
| 138 |
>
|
| 139 |
{dockItems.map((item, index) => (
|
| 140 |
-
<
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
<DockItem
|
| 146 |
-
icon={
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
}
|
| 151 |
-
label="Trash"
|
| 152 |
-
onClick={() => { }}
|
| 153 |
-
className=""
|
| 154 |
mouseX={mouseX}
|
| 155 |
/>
|
| 156 |
-
|
| 157 |
-
)}
|
| 158 |
-
|
| 159 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
</div>
|
| 161 |
</div>
|
| 162 |
)
|
|
|
|
| 12 |
Compass
|
| 13 |
} from '@phosphor-icons/react'
|
| 14 |
import { motion } from 'framer-motion'
|
| 15 |
+
import { DynamicClockIcon } from './DynamicClockIcon'
|
| 16 |
+
import { DynamicCalendarIcon } from './DynamicCalendarIcon'
|
| 17 |
+
|
| 18 |
+
interface MinimizedApp {
|
| 19 |
+
id: string
|
| 20 |
+
label: string
|
| 21 |
+
icon: React.ReactNode
|
| 22 |
+
onRestore: () => void
|
| 23 |
+
}
|
| 24 |
|
| 25 |
interface DockProps {
|
| 26 |
onOpenFileManager: (path: string) => void
|
|
|
|
| 29 |
onOpenBrowser: () => void
|
| 30 |
onOpenGeminiChat: () => void
|
| 31 |
openApps: { [key: string]: boolean }
|
| 32 |
+
minimizedApps?: MinimizedApp[]
|
| 33 |
}
|
| 34 |
|
| 35 |
interface DockItemProps {
|
|
|
|
| 72 |
onOpenClock,
|
| 73 |
onOpenBrowser,
|
| 74 |
onOpenGeminiChat,
|
| 75 |
+
openApps,
|
| 76 |
+
minimizedApps = []
|
| 77 |
}: DockProps) {
|
| 78 |
const mouseX = React.useRef<number | null>(null)
|
| 79 |
|
|
|
|
| 114 |
},
|
| 115 |
{
|
| 116 |
icon: (
|
| 117 |
+
<div className="w-full h-full">
|
| 118 |
+
<DynamicClockIcon />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
</div>
|
| 120 |
),
|
| 121 |
label: 'Clock',
|
|
|
|
| 125 |
},
|
| 126 |
{
|
| 127 |
icon: (
|
| 128 |
+
<div className="w-full h-full">
|
| 129 |
+
<DynamicCalendarIcon />
|
| 130 |
</div>
|
| 131 |
),
|
| 132 |
label: 'Calendar',
|
|
|
|
| 144 |
onMouseLeave={() => mouseX.current = null}
|
| 145 |
>
|
| 146 |
{dockItems.map((item, index) => (
|
| 147 |
+
<DockItem key={item.label} {...item} mouseX={mouseX} />
|
| 148 |
+
))}
|
| 149 |
+
|
| 150 |
+
{/* Minimized apps section */}
|
| 151 |
+
{minimizedApps.length > 0 && (
|
| 152 |
+
<>
|
| 153 |
+
<div className="w-px h-8 md:h-10 bg-gray-400/30 mx-0.5 md:mx-1" />
|
| 154 |
+
{minimizedApps.map((app) => (
|
| 155 |
+
<div key={app.id} className="relative">
|
| 156 |
<DockItem
|
| 157 |
+
icon={app.icon}
|
| 158 |
+
label={`${app.label} (minimized)`}
|
| 159 |
+
onClick={app.onRestore}
|
| 160 |
+
className="opacity-70 ring-2 ring-yellow-400/50"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
mouseX={mouseX}
|
| 162 |
/>
|
| 163 |
+
</div>
|
| 164 |
+
))}
|
| 165 |
+
</>
|
| 166 |
+
)}
|
| 167 |
+
|
| 168 |
+
{/* Trash */}
|
| 169 |
+
<div className="w-px h-8 md:h-10 bg-gray-400/30 mx-0.5 md:mx-1" />
|
| 170 |
+
<DockItem
|
| 171 |
+
icon={
|
| 172 |
+
<div className="bg-gradient-to-b from-gray-200 to-gray-300 w-full h-full rounded-xl flex items-center justify-center border border-white/40">
|
| 173 |
+
<Trash size={20} weight="regular" className="text-gray-600 md:scale-110" />
|
| 174 |
+
</div>
|
| 175 |
+
}
|
| 176 |
+
label="Trash"
|
| 177 |
+
onClick={() => { }}
|
| 178 |
+
className=""
|
| 179 |
+
mouseX={mouseX}
|
| 180 |
+
/>
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
)
|
app/components/DraggableDesktopIcon.tsx
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
| 15 |
Lightning,
|
| 16 |
Key
|
| 17 |
} from '@phosphor-icons/react'
|
|
|
|
|
|
|
| 18 |
|
| 19 |
interface DraggableDesktopIconProps {
|
| 20 |
id: string
|
|
@@ -53,14 +55,14 @@ export function DraggableDesktopIcon({
|
|
| 53 |
)
|
| 54 |
case 'calendar':
|
| 55 |
return (
|
| 56 |
-
<div className="
|
| 57 |
-
<
|
| 58 |
</div>
|
| 59 |
)
|
| 60 |
case 'clock':
|
| 61 |
return (
|
| 62 |
-
<div className="
|
| 63 |
-
<
|
| 64 |
</div>
|
| 65 |
)
|
| 66 |
case 'browser':
|
|
@@ -137,19 +139,17 @@ export function DraggableDesktopIcon({
|
|
| 137 |
>
|
| 138 |
<div className="icon-handle">
|
| 139 |
<div
|
| 140 |
-
className={`w-14 h-14 shadow-lg transition-transform hover:scale-110 ${
|
| 141 |
-
|
| 142 |
-
}`}
|
| 143 |
>
|
| 144 |
{getIcon()}
|
| 145 |
</div>
|
| 146 |
</div>
|
| 147 |
<span
|
| 148 |
-
className={`mt-1 text-xs font-medium drop-shadow-lg text-center leading-tight px-1 py-0.5 rounded transition-colors ${
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
}`}
|
| 153 |
>
|
| 154 |
{label}
|
| 155 |
</span>
|
|
|
|
| 15 |
Lightning,
|
| 16 |
Key
|
| 17 |
} from '@phosphor-icons/react'
|
| 18 |
+
import { DynamicClockIcon } from './DynamicClockIcon'
|
| 19 |
+
import { DynamicCalendarIcon } from './DynamicCalendarIcon'
|
| 20 |
|
| 21 |
interface DraggableDesktopIconProps {
|
| 22 |
id: string
|
|
|
|
| 55 |
)
|
| 56 |
case 'calendar':
|
| 57 |
return (
|
| 58 |
+
<div className="w-full h-full">
|
| 59 |
+
<DynamicCalendarIcon />
|
| 60 |
</div>
|
| 61 |
)
|
| 62 |
case 'clock':
|
| 63 |
return (
|
| 64 |
+
<div className="w-full h-full">
|
| 65 |
+
<DynamicClockIcon />
|
| 66 |
</div>
|
| 67 |
)
|
| 68 |
case 'browser':
|
|
|
|
| 139 |
>
|
| 140 |
<div className="icon-handle">
|
| 141 |
<div
|
| 142 |
+
className={`w-14 h-14 shadow-lg transition-transform hover:scale-110 ${iconType === 'clock' ? '' : 'rounded-xl'
|
| 143 |
+
}`}
|
|
|
|
| 144 |
>
|
| 145 |
{getIcon()}
|
| 146 |
</div>
|
| 147 |
</div>
|
| 148 |
<span
|
| 149 |
+
className={`mt-1 text-xs font-medium drop-shadow-lg text-center leading-tight px-1 py-0.5 rounded transition-colors ${selected
|
| 150 |
+
? 'bg-blue-600/80 text-white'
|
| 151 |
+
: 'text-white bg-black/20 group-hover:bg-blue-600/80'
|
| 152 |
+
}`}
|
|
|
|
| 153 |
>
|
| 154 |
{label}
|
| 155 |
</span>
|
app/components/DynamicCalendarIcon.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from 'react'
|
| 4 |
+
|
| 5 |
+
interface DynamicCalendarIconProps {
|
| 6 |
+
size?: number | string
|
| 7 |
+
className?: string
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function DynamicCalendarIcon({ size = '100%', className = '' }: DynamicCalendarIconProps) {
|
| 11 |
+
const [date, setDate] = useState<Date | null>(null)
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
setDate(new Date())
|
| 15 |
+
// Update date every minute to ensure it changes at midnight
|
| 16 |
+
const timer = setInterval(() => {
|
| 17 |
+
setDate(new Date())
|
| 18 |
+
}, 60000)
|
| 19 |
+
return () => clearInterval(timer)
|
| 20 |
+
}, [])
|
| 21 |
+
|
| 22 |
+
if (!date) return null
|
| 23 |
+
|
| 24 |
+
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase()
|
| 25 |
+
const dayNumber = date.getDate()
|
| 26 |
+
|
| 27 |
+
return (
|
| 28 |
+
<div
|
| 29 |
+
className={`flex flex-col items-center justify-center bg-white rounded-xl overflow-hidden shadow-lg border border-gray-200 ${className}`}
|
| 30 |
+
style={{ width: size, height: size }}
|
| 31 |
+
>
|
| 32 |
+
{/* Red Header */}
|
| 33 |
+
<div className="w-full bg-red-500 text-white font-bold flex items-center justify-center h-[30%] text-[0.6em] tracking-wide">
|
| 34 |
+
{dayName}
|
| 35 |
+
</div>
|
| 36 |
+
{/* Date Body */}
|
| 37 |
+
<div className="flex-1 flex items-center justify-center bg-white w-full pb-1">
|
| 38 |
+
<span className="text-black font-bold text-[1.4em] leading-none">{dayNumber}</span>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
)
|
| 42 |
+
}
|
app/components/DynamicClockIcon.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect } from 'react'
|
| 4 |
+
|
| 5 |
+
interface DynamicClockIconProps {
|
| 6 |
+
size?: number | string
|
| 7 |
+
className?: string
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function DynamicClockIcon({ size = '100%', className = '' }: DynamicClockIconProps) {
|
| 11 |
+
const [time, setTime] = useState<Date | null>(null)
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
setTime(new Date())
|
| 15 |
+
const timer = setInterval(() => {
|
| 16 |
+
setTime(new Date())
|
| 17 |
+
}, 1000)
|
| 18 |
+
return () => clearInterval(timer)
|
| 19 |
+
}, [])
|
| 20 |
+
|
| 21 |
+
if (!time) return null
|
| 22 |
+
|
| 23 |
+
const seconds = time.getSeconds()
|
| 24 |
+
const minutes = time.getMinutes()
|
| 25 |
+
const hours = time.getHours()
|
| 26 |
+
|
| 27 |
+
const secondDegrees = (seconds / 60) * 360
|
| 28 |
+
const minuteDegrees = ((minutes + seconds / 60) / 60) * 360
|
| 29 |
+
const hourDegrees = ((hours + minutes / 60) / 12) * 360
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<div
|
| 33 |
+
className={`relative bg-black rounded-full border-2 border-gray-600 shadow-lg flex items-center justify-center ${className}`}
|
| 34 |
+
style={{ width: size, height: size }}
|
| 35 |
+
>
|
| 36 |
+
<div className="relative w-full h-full rounded-full bg-black">
|
| 37 |
+
{/* Hour Hand */}
|
| 38 |
+
<div
|
| 39 |
+
className="absolute top-1/2 left-1/2 w-1 bg-white rounded-full origin-bottom"
|
| 40 |
+
style={{
|
| 41 |
+
height: '25%',
|
| 42 |
+
transform: `translate(-50%, -100%) rotate(${hourDegrees}deg)`
|
| 43 |
+
}}
|
| 44 |
+
/>
|
| 45 |
+
{/* Minute Hand */}
|
| 46 |
+
<div
|
| 47 |
+
className="absolute top-1/2 left-1/2 w-0.5 bg-gray-300 rounded-full origin-bottom"
|
| 48 |
+
style={{
|
| 49 |
+
height: '35%',
|
| 50 |
+
transform: `translate(-50%, -100%) rotate(${minuteDegrees}deg)`
|
| 51 |
+
}}
|
| 52 |
+
/>
|
| 53 |
+
{/* Second Hand */}
|
| 54 |
+
<div
|
| 55 |
+
className="absolute top-1/2 left-1/2 w-0.5 bg-orange-500 rounded-full origin-bottom"
|
| 56 |
+
style={{
|
| 57 |
+
height: '40%',
|
| 58 |
+
transform: `translate(-50%, -100%) rotate(${secondDegrees}deg)`
|
| 59 |
+
}}
|
| 60 |
+
/>
|
| 61 |
+
{/* Center Dot */}
|
| 62 |
+
<div className="absolute top-1/2 left-1/2 w-1.5 h-1.5 bg-orange-500 rounded-full transform -translate-x-1/2 -translate-y-1/2" />
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
)
|
| 66 |
+
}
|
app/components/FileManager.tsx
CHANGED
|
@@ -35,6 +35,7 @@ interface FileManagerProps {
|
|
| 35 |
onMinimize?: () => void
|
| 36 |
onMaximize?: () => void
|
| 37 |
onOpenFlutterApp?: (appFile: any) => void
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
interface FileItem {
|
|
@@ -49,7 +50,7 @@ interface FileItem {
|
|
| 49 |
pubspecYaml?: string
|
| 50 |
}
|
| 51 |
|
| 52 |
-
export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize }: FileManagerProps) {
|
| 53 |
const [files, setFiles] = useState<FileItem[]>([])
|
| 54 |
const [loading, setLoading] = useState(true)
|
| 55 |
const [searchQuery, setSearchQuery] = useState('')
|
|
@@ -58,12 +59,17 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 58 |
const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
|
| 59 |
const [isPublicFolder, setIsPublicFolder] = useState(false)
|
| 60 |
|
| 61 |
-
// Load files when path changes
|
| 62 |
useEffect(() => {
|
| 63 |
// Check if this is the public folder
|
| 64 |
setIsPublicFolder(currentPath === 'public' || currentPath.startsWith('public/'))
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
const loadFiles = async () => {
|
| 69 |
setLoading(true)
|
|
@@ -74,12 +80,22 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 74 |
const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '')
|
| 75 |
response = await fetch(`/api/public?folder=${encodeURIComponent(publicPath)}`)
|
| 76 |
} else {
|
| 77 |
-
// Load from regular files API
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
|
| 81 |
const data = await response.json()
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
// Add public folder to root directory
|
| 84 |
if (currentPath === '') {
|
| 85 |
const publicFolder = {
|
|
@@ -90,8 +106,12 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 90 |
}
|
| 91 |
|
| 92 |
// Add public folder if it doesn't exist
|
| 93 |
-
if (
|
| 94 |
-
data.files.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
}
|
| 97 |
|
|
@@ -153,8 +173,13 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 153 |
if (!confirm(`Delete ${file.name}?`)) return
|
| 154 |
|
| 155 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
const response = await fetch(`/api/files?path=${encodeURIComponent(file.path)}`, {
|
| 157 |
-
method: 'DELETE'
|
|
|
|
| 158 |
})
|
| 159 |
|
| 160 |
const result = await response.json()
|
|
@@ -174,9 +199,13 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 174 |
if (!folderName) return
|
| 175 |
|
| 176 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
const response = await fetch('/api/files', {
|
| 178 |
method: 'POST',
|
| 179 |
-
headers
|
| 180 |
body: JSON.stringify({
|
| 181 |
folderName,
|
| 182 |
parentPath: currentPath
|
|
@@ -323,7 +352,11 @@ export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp
|
|
| 323 |
|
| 324 |
{/* File List */}
|
| 325 |
<div className="flex-1 overflow-auto p-4 bg-white">
|
| 326 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
<div className="flex items-center justify-center h-full text-[#999] text-sm">
|
| 328 |
Loading files...
|
| 329 |
</div>
|
|
|
|
| 35 |
onMinimize?: () => void
|
| 36 |
onMaximize?: () => void
|
| 37 |
onOpenFlutterApp?: (appFile: any) => void
|
| 38 |
+
sessionId?: string
|
| 39 |
}
|
| 40 |
|
| 41 |
interface FileItem {
|
|
|
|
| 50 |
pubspecYaml?: string
|
| 51 |
}
|
| 52 |
|
| 53 |
+
export function FileManager({ currentPath, onNavigate, onClose, onOpenFlutterApp, onMinimize, onMaximize, sessionId }: FileManagerProps) {
|
| 54 |
const [files, setFiles] = useState<FileItem[]>([])
|
| 55 |
const [loading, setLoading] = useState(true)
|
| 56 |
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
| 59 |
const [previewFile, setPreviewFile] = useState<FileItem | null>(null)
|
| 60 |
const [isPublicFolder, setIsPublicFolder] = useState(false)
|
| 61 |
|
| 62 |
+
// Load files when path changes or sessionId becomes available
|
| 63 |
useEffect(() => {
|
| 64 |
// Check if this is the public folder
|
| 65 |
setIsPublicFolder(currentPath === 'public' || currentPath.startsWith('public/'))
|
| 66 |
+
|
| 67 |
+
// Only load files if we have a sessionId (for non-public folders) or if it's a public folder
|
| 68 |
+
const isPublic = currentPath === 'public' || currentPath.startsWith('public/')
|
| 69 |
+
if (isPublic || sessionId) {
|
| 70 |
+
loadFiles()
|
| 71 |
+
}
|
| 72 |
+
}, [currentPath, sessionId])
|
| 73 |
|
| 74 |
const loadFiles = async () => {
|
| 75 |
setLoading(true)
|
|
|
|
| 80 |
const publicPath = currentPath === 'public' ? '' : currentPath.replace('public/', '')
|
| 81 |
response = await fetch(`/api/public?folder=${encodeURIComponent(publicPath)}`)
|
| 82 |
} else {
|
| 83 |
+
// Load from regular files API with session headers
|
| 84 |
+
const headers: Record<string, string> = {}
|
| 85 |
+
if (sessionId) {
|
| 86 |
+
headers['x-session-id'] = sessionId
|
| 87 |
+
}
|
| 88 |
+
response = await fetch(`/api/files?folder=${encodeURIComponent(currentPath)}`, { headers })
|
| 89 |
}
|
| 90 |
|
| 91 |
const data = await response.json()
|
| 92 |
|
| 93 |
+
if (data.error) {
|
| 94 |
+
console.error('Error loading files:', data.error)
|
| 95 |
+
setFiles([])
|
| 96 |
+
return
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
// Add public folder to root directory
|
| 100 |
if (currentPath === '') {
|
| 101 |
const publicFolder = {
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
// Add public folder if it doesn't exist
|
| 109 |
+
if (data.files && Array.isArray(data.files)) {
|
| 110 |
+
if (!data.files.some((f: FileItem) => f.path === 'public')) {
|
| 111 |
+
data.files.unshift(publicFolder)
|
| 112 |
+
}
|
| 113 |
+
} else {
|
| 114 |
+
data.files = [publicFolder]
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
|
|
|
| 173 |
if (!confirm(`Delete ${file.name}?`)) return
|
| 174 |
|
| 175 |
try {
|
| 176 |
+
const headers: Record<string, string> = {}
|
| 177 |
+
if (sessionId) {
|
| 178 |
+
headers['x-session-id'] = sessionId
|
| 179 |
+
}
|
| 180 |
const response = await fetch(`/api/files?path=${encodeURIComponent(file.path)}`, {
|
| 181 |
+
method: 'DELETE',
|
| 182 |
+
headers
|
| 183 |
})
|
| 184 |
|
| 185 |
const result = await response.json()
|
|
|
|
| 199 |
if (!folderName) return
|
| 200 |
|
| 201 |
try {
|
| 202 |
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
| 203 |
+
if (sessionId) {
|
| 204 |
+
headers['x-session-id'] = sessionId
|
| 205 |
+
}
|
| 206 |
const response = await fetch('/api/files', {
|
| 207 |
method: 'POST',
|
| 208 |
+
headers,
|
| 209 |
body: JSON.stringify({
|
| 210 |
folderName,
|
| 211 |
parentPath: currentPath
|
|
|
|
| 352 |
|
| 353 |
{/* File List */}
|
| 354 |
<div className="flex-1 overflow-auto p-4 bg-white">
|
| 355 |
+
{!sessionId && !isPublicFolder ? (
|
| 356 |
+
<div className="flex items-center justify-center h-full text-[#999] text-sm">
|
| 357 |
+
Initializing session...
|
| 358 |
+
</div>
|
| 359 |
+
) : loading ? (
|
| 360 |
<div className="flex items-center justify-center h-full text-[#999] text-sm">
|
| 361 |
Loading files...
|
| 362 |
</div>
|
app/components/FlutterCodeEditor.tsx
CHANGED
|
@@ -267,7 +267,7 @@ class MyApp extends StatelessWidget {
|
|
| 267 |
>
|
| 268 |
<div className="flex flex-col h-full bg-gray-900">
|
| 269 |
{/* Header Toolbar */}
|
| 270 |
-
<div className="h-14 bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 flex items-center justify-between px-4 shadow-lg">
|
| 271 |
<div className="flex items-center gap-3">
|
| 272 |
<CodeIcon size={28} weight="bold" className="text-white" />
|
| 273 |
<span className="text-white font-bold text-lg">Flutter IDE</span>
|
|
|
|
| 267 |
>
|
| 268 |
<div className="flex flex-col h-full bg-gray-900">
|
| 269 |
{/* Header Toolbar */}
|
| 270 |
+
<div className="window-drag-handle h-14 bg-gradient-to-r from-blue-600 via-purple-600 to-pink-600 flex items-center justify-between px-4 shadow-lg cursor-move">
|
| 271 |
<div className="flex items-center gap-3">
|
| 272 |
<CodeIcon size={28} weight="bold" className="text-white" />
|
| 273 |
<span className="text-white font-bold text-lg">Flutter IDE</span>
|
app/components/FlutterRunner.tsx
CHANGED
|
@@ -96,6 +96,7 @@ export function FlutterRunner({ file, onClose, onMinimize, onMaximize }: Flutter
|
|
| 96 |
<Window
|
| 97 |
id="flutter-runner"
|
| 98 |
title={`Flutter App: ${file.name}`}
|
|
|
|
| 99 |
onClose={onClose}
|
| 100 |
onMinimize={onMinimize}
|
| 101 |
onMaximize={onMaximize}
|
|
@@ -107,7 +108,7 @@ export function FlutterRunner({ file, onClose, onMinimize, onMaximize }: Flutter
|
|
| 107 |
>
|
| 108 |
<div className="flex flex-col h-full bg-[#1E1E1E] text-gray-300">
|
| 109 |
{/* Top Toolbar */}
|
| 110 |
-
<div className="h-14 bg-[#252526] border-b border-[#333] flex items-center justify-between px-4 shadow-sm">
|
| 111 |
<div className="flex items-center gap-3">
|
| 112 |
<div className="w-8 h-8 bg-blue-500/20 rounded-lg flex items-center justify-center">
|
| 113 |
<CodeIcon size={20} weight="bold" className="text-blue-400" />
|
|
|
|
| 96 |
<Window
|
| 97 |
id="flutter-runner"
|
| 98 |
title={`Flutter App: ${file.name}`}
|
| 99 |
+
isOpen={true}
|
| 100 |
onClose={onClose}
|
| 101 |
onMinimize={onMinimize}
|
| 102 |
onMaximize={onMaximize}
|
|
|
|
| 108 |
>
|
| 109 |
<div className="flex flex-col h-full bg-[#1E1E1E] text-gray-300">
|
| 110 |
{/* Top Toolbar */}
|
| 111 |
+
<div className="window-drag-handle h-14 bg-[#252526] border-b border-[#333] flex items-center justify-between px-4 shadow-sm cursor-move">
|
| 112 |
<div className="flex items-center gap-3">
|
| 113 |
<div className="w-8 h-8 bg-blue-500/20 rounded-lg flex items-center justify-center">
|
| 114 |
<CodeIcon size={20} weight="bold" className="text-blue-400" />
|
app/components/LaTeXEditor.tsx
CHANGED
|
@@ -339,7 +339,7 @@ This document demonstrates LaTeX capabilities for: ${prompt}
|
|
| 339 |
>
|
| 340 |
<div className="flex flex-col h-full bg-[#F5F5F7]">
|
| 341 |
{/* Header */}
|
| 342 |
-
<div className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shadow-sm z-10">
|
| 343 |
<div className="flex items-center gap-4">
|
| 344 |
<div className="w-10 h-10 bg-black rounded-xl flex items-center justify-center shadow-lg">
|
| 345 |
<MathOperations size={24} weight="bold" className="text-white" />
|
|
|
|
| 339 |
>
|
| 340 |
<div className="flex flex-col h-full bg-[#F5F5F7]">
|
| 341 |
{/* Header */}
|
| 342 |
+
<div className="window-drag-handle h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shadow-sm z-10 cursor-move">
|
| 343 |
<div className="flex items-center gap-4">
|
| 344 |
<div className="w-10 h-10 bg-black rounded-xl flex items-center justify-center shadow-lg">
|
| 345 |
<MathOperations size={24} weight="bold" className="text-white" />
|
app/components/Window.tsx
CHANGED
|
@@ -43,6 +43,7 @@ const Window: React.FC<WindowProps> = ({
|
|
| 43 |
const [currentPosition, setCurrentPosition] = React.useState({ x, y });
|
| 44 |
const [currentSize, setCurrentSize] = React.useState({ width, height });
|
| 45 |
const [isMobile, setIsMobile] = React.useState(false);
|
|
|
|
| 46 |
|
| 47 |
// Detect mobile device
|
| 48 |
useEffect(() => {
|
|
@@ -84,7 +85,12 @@ const Window: React.FC<WindowProps> = ({
|
|
| 84 |
|
| 85 |
const handleMaximize = () => {
|
| 86 |
if (!isMaximized) {
|
| 87 |
-
setPreviousSize({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
setIsMaximized(true);
|
| 89 |
} else {
|
| 90 |
setIsMaximized(false);
|
|
@@ -92,59 +98,53 @@ const Window: React.FC<WindowProps> = ({
|
|
| 92 |
if (onMaximize) onMaximize();
|
| 93 |
};
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
</div>
|
| 108 |
-
<span className="font-semibold text-gray-700 flex-1 text-center pr-16 text-sm">{title}</span>
|
| 109 |
-
</div>
|
| 110 |
-
<div className="flex-1 overflow-auto">{children}</div>
|
| 111 |
-
</div>
|
| 112 |
-
);
|
| 113 |
-
}
|
| 114 |
|
| 115 |
return (
|
| 116 |
<Rnd
|
| 117 |
-
|
| 118 |
-
x: currentPosition.x,
|
| 119 |
-
y: currentPosition.y,
|
| 120 |
-
width: typeof currentSize.width === 'number' ? currentSize.width : 800,
|
| 121 |
-
height: typeof currentSize.height === 'number' ? currentSize.height : 600,
|
| 122 |
-
}}
|
| 123 |
-
position={undefined}
|
| 124 |
-
size={undefined}
|
| 125 |
minWidth={400}
|
| 126 |
minHeight={300}
|
| 127 |
bounds="parent"
|
| 128 |
dragHandleClassName="window-drag-handle"
|
| 129 |
-
enableResizing={resizable}
|
|
|
|
|
|
|
| 130 |
onDragStop={(e, d) => {
|
| 131 |
-
|
|
|
|
| 132 |
}}
|
|
|
|
| 133 |
onResizeStop={(e, direction, ref, delta, position) => {
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
| 139 |
}}
|
| 140 |
-
className=
|
| 141 |
style={{ zIndex: 50 }}
|
| 142 |
>
|
| 143 |
<div
|
| 144 |
-
className={`h-full macos-window flex flex-col ${className}`}
|
| 145 |
>
|
| 146 |
<div
|
| 147 |
className={`window-drag-handle h-10 flex items-center px-4 space-x-4 border-b cursor-move ${headerClass} ${headerClassName}`}
|
|
|
|
| 148 |
>
|
| 149 |
<div className="flex space-x-2 group">
|
| 150 |
<div className="traffic-light traffic-close" onClick={onClose} />
|
|
|
|
| 43 |
const [currentPosition, setCurrentPosition] = React.useState({ x, y });
|
| 44 |
const [currentSize, setCurrentSize] = React.useState({ width, height });
|
| 45 |
const [isMobile, setIsMobile] = React.useState(false);
|
| 46 |
+
const [isDraggingOrResizing, setIsDraggingOrResizing] = React.useState(false);
|
| 47 |
|
| 48 |
// Detect mobile device
|
| 49 |
useEffect(() => {
|
|
|
|
| 85 |
|
| 86 |
const handleMaximize = () => {
|
| 87 |
if (!isMaximized) {
|
| 88 |
+
setPreviousSize({
|
| 89 |
+
width: currentSize.width,
|
| 90 |
+
height: currentSize.height,
|
| 91 |
+
x: currentPosition.x,
|
| 92 |
+
y: currentPosition.y
|
| 93 |
+
});
|
| 94 |
setIsMaximized(true);
|
| 95 |
} else {
|
| 96 |
setIsMaximized(false);
|
|
|
|
| 98 |
if (onMaximize) onMaximize();
|
| 99 |
};
|
| 100 |
|
| 101 |
+
// Calculate Rnd props based on state
|
| 102 |
+
const rndProps = isMaximized ? {
|
| 103 |
+
position: { x: 0, y: 32 }, // 32px for TopBar offset
|
| 104 |
+
size: { width: '100%', height: 'calc(100vh - 32px)' },
|
| 105 |
+
disableDragging: true,
|
| 106 |
+
enableResizing: false
|
| 107 |
+
} : {
|
| 108 |
+
position: currentPosition,
|
| 109 |
+
size: currentSize,
|
| 110 |
+
disableDragging: false,
|
| 111 |
+
enableResizing: true
|
| 112 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
return (
|
| 115 |
<Rnd
|
| 116 |
+
{...rndProps}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
minWidth={400}
|
| 118 |
minHeight={300}
|
| 119 |
bounds="parent"
|
| 120 |
dragHandleClassName="window-drag-handle"
|
| 121 |
+
enableResizing={!isMaximized && resizable}
|
| 122 |
+
disableDragging={isMaximized}
|
| 123 |
+
onDragStart={() => setIsDraggingOrResizing(true)}
|
| 124 |
onDragStop={(e, d) => {
|
| 125 |
+
setIsDraggingOrResizing(false);
|
| 126 |
+
if (!isMaximized) setCurrentPosition({ x: d.x, y: d.y });
|
| 127 |
}}
|
| 128 |
+
onResizeStart={() => setIsDraggingOrResizing(true)}
|
| 129 |
onResizeStop={(e, direction, ref, delta, position) => {
|
| 130 |
+
setIsDraggingOrResizing(false);
|
| 131 |
+
if (!isMaximized) {
|
| 132 |
+
setCurrentSize({
|
| 133 |
+
width: ref.offsetWidth,
|
| 134 |
+
height: ref.offsetHeight,
|
| 135 |
+
});
|
| 136 |
+
setCurrentPosition(position);
|
| 137 |
+
}
|
| 138 |
}}
|
| 139 |
+
className={`pointer-events-auto ${!isDraggingOrResizing ? 'transition-all duration-300 ease-in-out' : ''}`}
|
| 140 |
style={{ zIndex: 50 }}
|
| 141 |
>
|
| 142 |
<div
|
| 143 |
+
className={`h-full macos-window flex flex-col ${className} ${windowClass}`}
|
| 144 |
>
|
| 145 |
<div
|
| 146 |
className={`window-drag-handle h-10 flex items-center px-4 space-x-4 border-b cursor-move ${headerClass} ${headerClassName}`}
|
| 147 |
+
onDoubleClick={handleMaximize}
|
| 148 |
>
|
| 149 |
<div className="flex space-x-2 group">
|
| 150 |
<div className="traffic-light traffic-close" onClick={onClose} />
|
app/data/holidays.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Holiday {
|
| 2 |
+
id: string
|
| 3 |
+
title: string
|
| 4 |
+
date: string // MM-DD format for recurring, or YYYY-MM-DD for specific
|
| 5 |
+
type: 'religious' | 'national' | 'cultural' | 'personal' | 'other'
|
| 6 |
+
color: string
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const recurringHolidays: Holiday[] = [
|
| 10 |
+
// Fixed Date Holidays (MM-DD)
|
| 11 |
+
{ id: 'new-year', title: "New Year's Day", date: '01-01', type: 'national', color: 'bg-blue-500' },
|
| 12 |
+
{ id: 'epiphany', title: "Epiphany", date: '01-06', type: 'religious', color: 'bg-purple-500' },
|
| 13 |
+
{ id: 'orth-xmas', title: "Orthodox Christmas", date: '01-07', type: 'religious', color: 'bg-purple-500' },
|
| 14 |
+
{ id: 'mlk', title: "Martin Luther King Jr. Day", date: '01-20', type: 'national', color: 'bg-blue-500' }, // Approximate (3rd Mon) - simplifying for fixed list or need logic
|
| 15 |
+
{ id: 'australia', title: "Australia Day", date: '01-26', type: 'national', color: 'bg-blue-500' },
|
| 16 |
+
{ id: 'republic-india', title: "Republic Day (India)", date: '01-26', type: 'national', color: 'bg-orange-500' },
|
| 17 |
+
|
| 18 |
+
{ id: 'valentines', title: "Valentine's Day", date: '02-14', type: 'cultural', color: 'bg-pink-500' },
|
| 19 |
+
{ id: 'presidents', title: "Presidents' Day", date: '02-17', type: 'national', color: 'bg-blue-500' }, // Approximate
|
| 20 |
+
|
| 21 |
+
{ id: 'womens', title: "Intl. Women's Day", date: '03-08', type: 'cultural', color: 'bg-pink-500' },
|
| 22 |
+
{ id: 'st-patrick', title: "St. Patrick's Day", date: '03-17', type: 'cultural', color: 'bg-green-500' },
|
| 23 |
+
{ id: 'nowruz', title: "Nowruz", date: '03-21', type: 'cultural', color: 'bg-green-500' },
|
| 24 |
+
|
| 25 |
+
{ id: 'earth', title: "Earth Day", date: '04-22', type: 'cultural', color: 'bg-green-600' },
|
| 26 |
+
{ id: 'anzac', title: "ANZAC Day", date: '04-25', type: 'national', color: 'bg-red-500' },
|
| 27 |
+
|
| 28 |
+
{ id: 'labor', title: "Labor Day (Intl)", date: '05-01', type: 'national', color: 'bg-red-500' },
|
| 29 |
+
{ id: 'cinco', title: "Cinco de Mayo", date: '05-05', type: 'cultural', color: 'bg-green-500' },
|
| 30 |
+
{ id: 'victory-eu', title: "Victory Day (Europe)", date: '05-08', type: 'national', color: 'bg-blue-500' },
|
| 31 |
+
|
| 32 |
+
{ id: 'juneteenth', title: "Juneteenth", date: '06-19', type: 'national', color: 'bg-blue-500' },
|
| 33 |
+
{ id: 'yoga', title: "Intl. Day of Yoga", date: '06-21', type: 'cultural', color: 'bg-orange-500' },
|
| 34 |
+
|
| 35 |
+
{ id: 'canada', title: "Canada Day", date: '07-01', type: 'national', color: 'bg-red-500' },
|
| 36 |
+
{ id: 'us-indep', title: "Independence Day (USA)", date: '07-04', type: 'national', color: 'bg-blue-500' },
|
| 37 |
+
{ id: 'bastille', title: "Bastille Day", date: '07-14', type: 'national', color: 'bg-blue-500' },
|
| 38 |
+
|
| 39 |
+
{ id: 'indep-india', title: "Independence Day (India)", date: '08-15', type: 'national', color: 'bg-orange-500' },
|
| 40 |
+
|
| 41 |
+
{ id: 'reuben-bday', title: "Reuben's Birthday", date: '09-16', type: 'personal', color: 'bg-gradient-to-r from-purple-500 to-pink-500' },
|
| 42 |
+
|
| 43 |
+
{ id: 'german-unity', title: "German Unity Day", date: '10-03', type: 'national', color: 'bg-yellow-500' },
|
| 44 |
+
{ id: 'halloween', title: "Halloween", date: '10-31', type: 'cultural', color: 'bg-orange-500' },
|
| 45 |
+
|
| 46 |
+
{ id: 'saints', title: "All Saints' Day", date: '11-01', type: 'religious', color: 'bg-purple-500' },
|
| 47 |
+
{ id: 'souls', title: "All Souls' Day", date: '11-02', type: 'religious', color: 'bg-purple-500' },
|
| 48 |
+
{ id: 'remembrance', title: "Remembrance Day", date: '11-11', type: 'national', color: 'bg-red-500' },
|
| 49 |
+
{ id: 'veterans', title: "Veterans Day", date: '11-11', type: 'national', color: 'bg-blue-500' },
|
| 50 |
+
|
| 51 |
+
{ id: 'xmas', title: "Christmas Day", date: '12-25', type: 'religious', color: 'bg-green-600' },
|
| 52 |
+
{ id: 'boxing', title: "Boxing Day", date: '12-26', type: 'cultural', color: 'bg-blue-500' },
|
| 53 |
+
{ id: 'nye', title: "New Year's Eve", date: '12-31', type: 'cultural', color: 'bg-purple-500' },
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
// Function to generate variable date holidays (Easter, Diwali, Eid, etc.)
|
| 57 |
+
// This is a simplified approximation for demonstration purposes
|
| 58 |
+
export const getVariableHolidays = (year: number): Holiday[] => {
|
| 59 |
+
const holidays: Holiday[] = []
|
| 60 |
+
|
| 61 |
+
// Easter (Western) - Simplified algorithm
|
| 62 |
+
const a = year % 19
|
| 63 |
+
const b = Math.floor(year / 100)
|
| 64 |
+
const c = year % 100
|
| 65 |
+
const d = Math.floor(b / 4)
|
| 66 |
+
const e = b % 4
|
| 67 |
+
const f = Math.floor((b + 8) / 25)
|
| 68 |
+
const g = Math.floor((b - f + 1) / 3)
|
| 69 |
+
const h = (19 * a + b - d - g + 15) % 30
|
| 70 |
+
const i = Math.floor(c / 4)
|
| 71 |
+
const k = c % 4
|
| 72 |
+
const l = (32 + 2 * e + 2 * i - h - k) % 7
|
| 73 |
+
const m = Math.floor((a + 11 * h + 22 * l) / 451)
|
| 74 |
+
const month = Math.floor((h + l - 7 * m + 114) / 31)
|
| 75 |
+
const day = ((h + l - 7 * m + 114) % 31) + 1
|
| 76 |
+
|
| 77 |
+
const easterDate = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
| 78 |
+
holidays.push({ id: `easter-${year}`, title: "Easter Sunday", date: easterDate, type: 'religious', color: 'bg-purple-500' })
|
| 79 |
+
|
| 80 |
+
// Good Friday (2 days before Easter)
|
| 81 |
+
const gfDate = new Date(year, month - 1, day - 2)
|
| 82 |
+
holidays.push({
|
| 83 |
+
id: `good-friday-${year}`,
|
| 84 |
+
title: "Good Friday",
|
| 85 |
+
date: `${year}-${String(gfDate.getMonth() + 1).padStart(2, '0')}-${String(gfDate.getDate()).padStart(2, '0')}`,
|
| 86 |
+
type: 'religious',
|
| 87 |
+
color: 'bg-purple-500'
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
// Thanksgiving (USA) - 4th Thursday of November
|
| 91 |
+
const nov1 = new Date(year, 10, 1)
|
| 92 |
+
const dayOfWeek = nov1.getDay() // 0=Sun, 1=Mon...
|
| 93 |
+
const offset = (4 - dayOfWeek + 7) % 7 // Days to first Thursday
|
| 94 |
+
const thanksgivingDay = 1 + offset + 21 // 4th Thursday
|
| 95 |
+
holidays.push({
|
| 96 |
+
id: `thanksgiving-${year}`,
|
| 97 |
+
title: "Thanksgiving (USA)",
|
| 98 |
+
date: `${year}-11-${String(thanksgivingDay).padStart(2, '0')}`,
|
| 99 |
+
type: 'national',
|
| 100 |
+
color: 'bg-orange-600'
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
// Diwali (Approximation - usually Oct/Nov)
|
| 104 |
+
// Hardcoding for 2024-2026 for demo
|
| 105 |
+
if (year === 2024) holidays.push({ id: 'diwali-24', title: "Diwali", date: '2024-11-01', type: 'religious', color: 'bg-orange-500' })
|
| 106 |
+
if (year === 2025) holidays.push({ id: 'diwali-25', title: "Diwali", date: '2025-10-20', type: 'religious', color: 'bg-orange-500' })
|
| 107 |
+
if (year === 2026) holidays.push({ id: 'diwali-26', title: "Diwali", date: '2026-11-08', type: 'religious', color: 'bg-orange-500' })
|
| 108 |
+
|
| 109 |
+
// Eid al-Fitr (Approximation)
|
| 110 |
+
if (year === 2024) holidays.push({ id: 'eid-24', title: "Eid al-Fitr", date: '2024-04-10', type: 'religious', color: 'bg-green-600' })
|
| 111 |
+
if (year === 2025) holidays.push({ id: 'eid-25', title: "Eid al-Fitr", date: '2025-03-31', type: 'religious', color: 'bg-green-600' })
|
| 112 |
+
|
| 113 |
+
// Lunar New Year
|
| 114 |
+
if (year === 2024) holidays.push({ id: 'lny-24', title: "Lunar New Year", date: '2024-02-10', type: 'cultural', color: 'bg-red-600' })
|
| 115 |
+
if (year === 2025) holidays.push({ id: 'lny-25', title: "Lunar New Year", date: '2025-01-29', type: 'cultural', color: 'bg-red-600' })
|
| 116 |
+
|
| 117 |
+
// Other specific festivals mentioned in prompt/image
|
| 118 |
+
if (year === 2025) {
|
| 119 |
+
holidays.push({ id: 'chhath-25', title: "Chhath Puja", date: '2025-10-27', type: 'religious', color: 'bg-blue-400' })
|
| 120 |
+
holidays.push({ id: 'culture-jp-25', title: "Culture Day (Japan)", date: '2025-11-03', type: 'cultural', color: 'bg-blue-400' })
|
| 121 |
+
holidays.push({ id: 'guru-nanak-25', title: "Guru Nanak Jayanti", date: '2025-11-05', type: 'religious', color: 'bg-purple-400' })
|
| 122 |
+
holidays.push({ id: 'guru-tegh-25', title: "Guru Tegh Bahadur's Martyrdom Day", date: '2025-11-24', type: 'religious', color: 'bg-purple-400' })
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return holidays
|
| 126 |
+
}
|