Reubencf commited on
Commit
bc139ec
·
1 Parent(s): ad31128

few changes like calendar and clock

Browse files
app/components/Calendar.tsx CHANGED
@@ -1,10 +1,19 @@
1
  'use client'
2
 
3
- import React, { useState } from 'react'
4
- import { CaretLeft, CaretRight } from '@phosphor-icons/react'
5
- import { motion } from 'framer-motion'
 
 
 
 
 
 
 
 
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 Event {
16
- id: string
17
- date: string
18
- title: string
19
- type: 'holiday' | 'custom'
20
  }
21
 
22
- const defaultHolidays: Event[] = [
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 [events, setEvents] = useKV<Event[]>('calendar-events', defaultHolidays)
39
- const [selectedDay, setSelectedDay] = useState<number | null>(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  const monthNames = [
42
  'January', 'February', 'March', 'April', 'May', 'June',
43
  'July', 'August', 'September', 'October', 'November', 'December'
44
  ]
45
 
46
- const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
 
47
 
48
- const getDaysInMonth = (date: Date) => {
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
- const days: (number | null)[] = []
57
- for (let i = 0; i < startingDayOfWeek; i++) {
58
- days.push(null)
 
 
 
 
 
 
 
59
  }
 
 
 
60
  for (let i = 1; i <= daysInMonth; i++) {
61
- days.push(i)
 
 
 
 
 
62
  }
63
- return days
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
- const days = getDaysInMonth(currentDate)
67
- const today = new Date()
68
- const isToday = (day: number | null) => {
69
- if (!day) return false
70
- return (
71
- day === today.getDate() &&
72
- currentDate.getMonth() === today.getMonth() &&
73
- currentDate.getFullYear() === today.getFullYear()
74
- )
75
  }
76
 
77
- const getEventsForDay = (day: number | null) => {
78
- if (!day || !events) return []
79
- const dateStr = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
80
- return events.filter(event => event.date === dateStr)
81
  }
82
 
83
- const hasEvent = (day: number | null) => {
84
- return getEventsForDay(day).length > 0
 
 
 
 
 
 
 
 
 
 
85
  }
86
 
87
- const previousMonth = () => {
88
- setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))
89
- setSelectedDay(null)
 
 
 
 
 
 
 
 
 
90
  }
91
 
92
- const nextMonth = () => {
93
- setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))
94
- setSelectedDay(null)
95
  }
96
 
97
- const handleDayClick = (day: number | null) => {
98
- if (day) {
99
- setSelectedDay(day)
 
 
 
 
 
 
 
 
 
100
  }
 
101
  }
102
 
103
- const selectedDayEvents = selectedDay ? getEventsForDay(selectedDay) : []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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={800}
114
- height={500}
115
- x={100}
116
- y={100}
117
  className="calendar-window"
118
  >
119
- <div className="p-6 h-full flex flex-col">
120
- <div className="flex items-center justify-between mb-4">
121
- <button
122
- onClick={previousMonth}
123
- className="p-2 hover:bg-[#f0f0f0] rounded"
124
- >
125
- <CaretLeft size={20} weight="bold" />
126
- </button>
127
- <div className="text-base font-semibold text-[#2c2c2c]">
128
- {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <div className="grid grid-cols-7 gap-1 mb-2">
139
- {daysOfWeek.map((day) => (
140
- <div
141
- key={day}
142
- className="text-center text-xs font-medium text-[#666] py-1"
 
 
 
 
 
143
  >
144
- {day}
145
- </div>
146
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
 
149
- <div className="grid grid-cols-7 gap-1">
150
- {days.map((day, index) => (
151
- day ? (
152
- <button
153
- key={index}
154
- onClick={() => handleDayClick(day)}
155
- className={`
156
- aspect-square flex flex-col items-center justify-center text-sm rounded relative
157
- hover:bg-[#f0f0f0]
158
- ${isToday(day) ? 'bg-[#E95420] text-white hover:bg-[#d14818] font-bold' : 'text-[#2c2c2c]'}
159
- ${selectedDay === day && !isToday(day) ? 'bg-[#f0f0f0] ring-2 ring-[#E95420]' : ''}
160
- `}
161
- >
162
- <span>{day}</span>
163
- {hasEvent(day) && (
164
- <div className="absolute bottom-1 flex gap-0.5">
165
- {getEventsForDay(day).slice(0, 3).map((event, i) => (
166
- <div
167
- key={i}
168
- className={`w-1 h-1 rounded-full ${event.type === 'holiday'
169
- ? isToday(day) ? 'bg-white' : 'bg-purple-500'
170
- : isToday(day) ? 'bg-white' : 'bg-blue-500'
171
- }`}
172
- />
173
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
- )}
176
- </button>
177
- ) : (
178
- <div
179
- key={index}
180
- className="aspect-square"
181
- />
182
- )
183
- ))}
184
- </div>
185
 
186
- {selectedDayEvents.length > 0 && (
187
- <div className="mt-4 pt-4 border-t border-[#e0e0e0] flex-1 overflow-y-auto">
188
- <div className="text-xs text-[#666] mb-2">
189
- {monthNames[currentDate.getMonth()]} {selectedDay}, {currentDate.getFullYear()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  </div>
191
- <div className="space-y-2">
192
- {selectedDayEvents.map((event) => (
193
- <div
194
- key={event.id}
195
- className="flex items-start gap-2 p-2 bg-[#f9f9f9] rounded"
196
- >
197
- <div
198
- className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${event.type === 'holiday' ? 'bg-purple-500' : 'bg-blue-500'
199
- }`}
200
- />
201
- <div className="flex-1">
202
- <div className="text-sm font-medium text-[#2c2c2c]">
203
- {event.title}
 
 
 
 
 
 
 
204
  </div>
205
- <div className="text-xs text-[#666]">
206
- {event.type === 'holiday' ? 'Holiday' : 'Event'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- {getEventsForDay(today.getDate()).length > 0 && (
227
- <div className="mt-3 space-y-2">
228
- {getEventsForDay(today.getDate()).map((event) => (
229
- <div
230
- key={event.id}
231
- className="flex items-start gap-2 p-2 bg-[#fff3e6] rounded"
232
- >
 
 
 
233
  <div
234
- className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${event.type === 'holiday' ? 'bg-purple-500' : 'bg-blue-500'
235
- }`}
236
- />
237
- <div className="flex-1">
238
- <div className="text-sm font-medium text-[#2c2c2c]">
239
- {event.title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  </div>
241
  </div>
242
- </div>
243
- ))}
244
  </div>
245
- )}
246
- </div>
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.95, opacity: 0 }}
536
  animate={{
537
- scale: fileManagerMinimized ? 0 : 1,
538
  opacity: fileManagerMinimized ? 0 : 1,
539
- y: fileManagerMinimized ? 200 : 0
 
540
  }}
541
- exit={{ scale: 0.95, opacity: 0 }}
542
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
560
  animate={{
561
- scale: calendarMinimized ? 0 : 1,
562
  opacity: calendarMinimized ? 0 : 1,
563
- y: calendarMinimized ? 200 : 0
 
564
  }}
565
- exit={{ scale: 0.95, opacity: 0 }}
566
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
578
  animate={{
579
- scale: clockMinimized ? 0 : 1,
580
  opacity: clockMinimized ? 0 : 1,
581
- y: clockMinimized ? 200 : 0
 
582
  }}
583
- exit={{ scale: 0.95, opacity: 0 }}
584
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
596
  animate={{
597
- scale: browserMinimized ? 0 : 1,
598
  opacity: browserMinimized ? 0 : 1,
599
- y: browserMinimized ? 200 : 0
 
600
  }}
601
- exit={{ scale: 0.95, opacity: 0 }}
602
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
614
  animate={{
615
- scale: terminalMinimized ? 0 : 1,
616
  opacity: terminalMinimized ? 0 : 1,
617
- y: terminalMinimized ? 200 : 0
 
618
  }}
619
- exit={{ scale: 0.95, opacity: 0 }}
620
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.9, opacity: 0 }}
632
  animate={{
633
- scale: sessionManagerMinimized ? 0 : 1,
634
  opacity: sessionManagerMinimized ? 0 : 1,
635
- y: sessionManagerMinimized ? 200 : 0
 
636
  }}
637
- exit={{ scale: 0.9, opacity: 0 }}
638
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
655
  animate={{
656
- scale: flutterRunnerMinimized ? 0 : 1,
657
  opacity: flutterRunnerMinimized ? 0 : 1,
658
- y: flutterRunnerMinimized ? 200 : 0
 
659
  }}
660
- exit={{ scale: 0.95, opacity: 0 }}
661
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
677
  animate={{
678
- scale: researchBrowserMinimized ? 0 : 1,
679
  opacity: researchBrowserMinimized ? 0 : 1,
680
- y: researchBrowserMinimized ? 200 : 0
 
681
  }}
682
- exit={{ scale: 0.95, opacity: 0 }}
683
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
698
  animate={{
699
- scale: flutterCodeEditorMinimized ? 0 : 1,
700
  opacity: flutterCodeEditorMinimized ? 0 : 1,
701
- y: flutterCodeEditorMinimized ? 200 : 0
 
702
  }}
703
- exit={{ scale: 0.95, opacity: 0 }}
704
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
716
  animate={{
717
- scale: latexEditorMinimized ? 0 : 1,
718
  opacity: latexEditorMinimized ? 0 : 1,
719
- y: latexEditorMinimized ? 200 : 0
 
720
  }}
721
- exit={{ scale: 0.95, opacity: 0 }}
722
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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.95, opacity: 0 }}
733
  animate={{
734
- scale: geminiChatMinimized ? 0 : 1,
735
  opacity: geminiChatMinimized ? 0 : 1,
736
- y: geminiChatMinimized ? 200 : 0
 
737
  }}
738
- exit={{ scale: 0.95, opacity: 0 }}
739
- transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
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="bg-black w-full h-full rounded-full flex items-center justify-center border border-gray-600">
107
- <div className="w-10 h-10 rounded-full border border-gray-600 relative bg-black">
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="bg-gradient-to-br from-purple-500 to-purple-600 w-full h-full rounded-xl flex items-center justify-center shadow-inner">
122
- <CalendarIcon size={24} weight="regular" className="text-white md:scale-110" />
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
- <React.Fragment key={item.label}>
141
- <DockItem {...item} mouseX={mouseX} />
142
- {index === dockItems.length - 1 && (
143
- <>
144
- <div className="w-px h-8 md:h-10 bg-gray-400/30 mx-0.5 md:mx-1" />
 
 
 
 
145
  <DockItem
146
- icon={
147
- <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">
148
- <Trash size={20} weight="regular" className="text-gray-600 md:scale-110" />
149
- </div>
150
- }
151
- label="Trash"
152
- onClick={() => { }}
153
- className=""
154
  mouseX={mouseX}
155
  />
156
- </>
157
- )}
158
- </React.Fragment>
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="bg-gradient-to-br from-purple-500 to-purple-600 w-full h-full rounded-xl flex items-center justify-center shadow-inner">
57
- <Calendar size={32} weight="regular" className="text-white" />
58
  </div>
59
  )
60
  case 'clock':
61
  return (
62
- <div className="bg-black w-full h-full rounded-full flex items-center justify-center border-2 border-gray-600">
63
- <Clock size={32} weight="regular" className="text-white" />
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
- iconType === 'clock' ? '' : 'rounded-xl'
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
- 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>
 
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
- loadFiles()
66
- }, [currentPath])
 
 
 
 
 
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
- response = await fetch(`/api/files?folder=${encodeURIComponent(currentPath)}`)
 
 
 
 
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 (!data.files.some((f: FileItem) => f.path === 'public')) {
94
- data.files.unshift(publicFolder)
 
 
 
 
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: { 'Content-Type': 'application/json' },
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
- {loading ? (
 
 
 
 
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({ width, height, x, y });
 
 
 
 
 
88
  setIsMaximized(true);
89
  } else {
90
  setIsMaximized(false);
@@ -92,59 +98,53 @@ const Window: React.FC<WindowProps> = ({
92
  if (onMaximize) onMaximize();
93
  };
94
 
95
- if (isMaximized) {
96
- return (
97
- <div
98
- className={`fixed inset-0 top-8 z-50 flex flex-col overflow-hidden ${windowClass} ${className}`}
99
- >
100
- <div
101
- className={`h-10 flex items-center px-4 space-x-4 border-b ${headerClass} ${headerClassName}`}
102
- >
103
- <div className="flex space-x-2 group">
104
- <div className="traffic-light traffic-close" onClick={onClose} />
105
- <div className="traffic-light traffic-min" onClick={onMinimize} />
106
- <div className="traffic-light traffic-max" onClick={handleMaximize} />
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
- default={{
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
- setCurrentPosition({ x: d.x, y: d.y });
 
132
  }}
 
133
  onResizeStop={(e, direction, ref, delta, position) => {
134
- setCurrentSize({
135
- width: ref.offsetWidth,
136
- height: ref.offsetHeight,
137
- });
138
- setCurrentPosition(position);
 
 
 
139
  }}
140
- className="pointer-events-auto"
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
+ }