Reubencf commited on
Commit
062c414
·
1 Parent(s): a677ebd

Added messaging appLets Chat

Browse files
app/api/gemini/chat/route.ts CHANGED
@@ -45,7 +45,7 @@ export async function POST(request: NextRequest) {
45
  // Process each part to separate thoughts from actual response
46
  let text = ''
47
  let thought = ''
48
-
49
  if (chunk.candidates?.[0]?.content?.parts) {
50
  for (const part of chunk.candidates[0].content.parts) {
51
  if (!part.text) {
@@ -76,7 +76,7 @@ export async function POST(request: NextRequest) {
76
 
77
  return new NextResponse(stream, {
78
  headers: {
79
- 'Content-Type': 'text/event-stream',
80
  'Cache-Control': 'no-cache',
81
  'Connection': 'keep-alive',
82
  },
 
45
  // Process each part to separate thoughts from actual response
46
  let text = ''
47
  let thought = ''
48
+
49
  if (chunk.candidates?.[0]?.content?.parts) {
50
  for (const part of chunk.candidates[0].content.parts) {
51
  if (!part.text) {
 
76
 
77
  return new NextResponse(stream, {
78
  headers: {
79
+ 'Content-Type': 'text/event-stream; charset=utf-8',
80
  'Cache-Control': 'no-cache',
81
  'Connection': 'keep-alive',
82
  },
app/api/messages/route.ts ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server'
2
+ import fs from 'fs'
3
+ import path from 'path'
4
+
5
+ const dataFilePath = path.join(process.cwd(), 'app/data/messages.json')
6
+
7
+ // Simple bad word list (expand as needed)
8
+ const BAD_WORDS = [
9
+ // English
10
+ 'badword', 'spam', 'toxic', 'hate', 'violence', 'kill', 'stupid', 'idiot',
11
+ 'fuck', 'fuxk', 'fck', 'shit', 'sh!t', 'bitch', 'asshole', 'damn', 'hell',
12
+ 'crap', 'piss', 'dick', 'cock', 'pussy', 'bastard', 'slut', 'whore',
13
+ // Spanish
14
+ 'puto', 'mierda', 'coño', 'cabron', 'pendejo', 'joder',
15
+ // French
16
+ 'merde', 'putain', 'connard', 'salope', 'encule',
17
+ // German
18
+ 'scheisse', 'arschloch', 'schlampe', 'fotze',
19
+ // Italian
20
+ 'cazzo', 'merda', 'vaffanculo', 'stronzo',
21
+ // Portuguese
22
+ 'porra', 'caralho', 'merda', 'puta',
23
+ // Russian (transliterated)
24
+ 'cyka', 'blyat', 'nahui', 'pizda',
25
+ // Hindi (transliterated)
26
+ 'madarchod', 'bhenchod', 'chutiya', 'kutta', 'kamina'
27
+ ]
28
+
29
+ interface Message {
30
+ id: string
31
+ text: string
32
+ sender: string
33
+ userId: string
34
+ timestamp: number
35
+ }
36
+
37
+ const getMessages = (): Message[] => {
38
+ try {
39
+ if (!fs.existsSync(dataFilePath)) {
40
+ fs.writeFileSync(dataFilePath, '[]', 'utf8')
41
+ return []
42
+ }
43
+ const fileData = fs.readFileSync(dataFilePath, 'utf8')
44
+ return JSON.parse(fileData)
45
+ } catch (error) {
46
+ console.error('Error reading messages:', error)
47
+ return []
48
+ }
49
+ }
50
+
51
+ const saveMessages = (messages: Message[]) => {
52
+ try {
53
+ fs.writeFileSync(dataFilePath, JSON.stringify(messages, null, 2), 'utf8')
54
+ } catch (error) {
55
+ console.error('Error saving messages:', error)
56
+ }
57
+ }
58
+
59
+ export async function GET() {
60
+ const messages = getMessages()
61
+ return NextResponse.json(messages, {
62
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
63
+ })
64
+ }
65
+
66
+ export async function POST(request: Request) {
67
+ try {
68
+ const body = await request.json()
69
+ const { text, sender, userId } = body
70
+
71
+ // 1. Validation: Length
72
+ if (!text || text.length > 200) {
73
+ return NextResponse.json({ error: 'Message too long (max 200 chars)' }, { status: 400 })
74
+ }
75
+
76
+ if (!text.trim()) {
77
+ return NextResponse.json({ error: 'Message cannot be empty' }, { status: 400 })
78
+ }
79
+
80
+ // 2. Validation: Toxicity
81
+ const lowerText = text.toLowerCase()
82
+ const containsBadWord = BAD_WORDS.some(word => lowerText.includes(word))
83
+ if (containsBadWord) {
84
+ return NextResponse.json({ error: 'Message contains inappropriate content' }, { status: 400 })
85
+ }
86
+
87
+ const messages = getMessages()
88
+ const now = Date.now()
89
+
90
+ // 3. Validation: Spam (Rate Limiting)
91
+ // Check if this user sent a message in the last 2 seconds
92
+ const lastMessageFromUser = messages
93
+ .filter(m => m.userId === userId)
94
+ .sort((a, b) => b.timestamp - a.timestamp)[0]
95
+
96
+ if (lastMessageFromUser && (now - lastMessageFromUser.timestamp) < 2000) {
97
+ return NextResponse.json({ error: 'You are sending messages too fast. Please wait.' }, { status: 429 })
98
+ }
99
+
100
+ // Check for duplicate message from same user
101
+ if (lastMessageFromUser && lastMessageFromUser.text === text) {
102
+ return NextResponse.json({ error: 'Do not send duplicate messages.' }, { status: 400 })
103
+ }
104
+
105
+ const newMessage: Message = {
106
+ id: Math.random().toString(36).substring(2, 15),
107
+ text: text.trim(),
108
+ sender: sender || 'Anonymous',
109
+ userId: userId,
110
+ timestamp: now
111
+ }
112
+
113
+ // Keep only last 100 messages to prevent file from growing too large
114
+ const updatedMessages = [...messages, newMessage].slice(-100)
115
+
116
+ saveMessages(updatedMessages)
117
+
118
+ return NextResponse.json(updatedMessages, {
119
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
120
+ })
121
+ } catch (error) {
122
+ console.error('Error processing message:', error)
123
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 })
124
+ }
125
+ }
app/components/Calendar.tsx CHANGED
@@ -6,9 +6,9 @@ import {
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'
@@ -32,24 +32,22 @@ type ViewMode = 'day' | 'week' | 'month' | 'year'
32
  export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: CalendarProps) {
33
  const [currentDate, setCurrentDate] = useState(new Date())
34
  const [view, setView] = useState<ViewMode>('month')
35
- const [selectedDate, setSelectedDate] = useState<Date | null>(null)
36
  const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
37
  const [searchQuery, setSearchQuery] = useState('')
38
  const [isSearchOpen, setIsSearchOpen] = useState(false)
39
  const scrollContainerRef = useRef<HTMLDivElement>(null)
 
40
 
41
  // Scroll to 8 AM on mount for day/week views
42
  useEffect(() => {
43
  if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
44
- // 8 AM is the 8th hour slot, assuming 60px height per hour
45
  scrollContainerRef.current.scrollTop = 8 * 60
46
  }
47
  }, [view])
48
 
49
- // Generate all events for the current view's year
50
  const allEvents = useMemo(() => {
51
  const year = currentDate.getFullYear()
52
- // Get holidays for previous, current, and next year to handle boundary transitions
53
  const years = [year - 1, year, year + 1]
54
  let events: CalendarEvent[] = [...customEvents]
55
 
@@ -77,9 +75,8 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
77
  const firstDay = new Date(year, month, 1)
78
  const lastDay = new Date(year, month + 1, 0)
79
  const daysInMonth = lastDay.getDate()
80
- const startingDayOfWeek = firstDay.getDay() // 0 = Sunday
81
 
82
- // Previous month days to fill the grid
83
  const prevMonthDays = []
84
  const prevMonthLastDay = new Date(year, month, 0).getDate()
85
  for (let i = startingDayOfWeek - 1; i >= 0; i--) {
@@ -91,7 +88,6 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
91
  })
92
  }
93
 
94
- // Current month days
95
  const currentMonthDays = []
96
  for (let i = 1; i <= daysInMonth; i++) {
97
  currentMonthDays.push({
@@ -102,10 +98,9 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
102
  })
103
  }
104
 
105
- // Next month days to complete the grid (6 rows * 7 cols = 42 cells usually)
106
  const nextMonthDays = []
107
  const totalDaysSoFar = prevMonthDays.length + currentMonthDays.length
108
- const remainingCells = 42 - totalDaysSoFar // Ensure 6 rows
109
 
110
  for (let i = 1; i <= remainingCells; i++) {
111
  nextMonthDays.push({
@@ -134,37 +129,24 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
134
 
135
  const handlePrev = () => {
136
  const newDate = new Date(currentDate)
137
- if (view === 'month') {
138
- newDate.setMonth(newDate.getMonth() - 1)
139
- } else if (view === 'year') {
140
- newDate.setFullYear(newDate.getFullYear() - 1)
141
- } else if (view === 'week') {
142
- newDate.setDate(newDate.getDate() - 7)
143
- } else if (view === 'day') {
144
- newDate.setDate(newDate.getDate() - 1)
145
- }
146
  setCurrentDate(newDate)
147
  }
148
 
149
  const handleNext = () => {
150
  const newDate = new Date(currentDate)
151
- if (view === 'month') {
152
- newDate.setMonth(newDate.getMonth() + 1)
153
- } else if (view === 'year') {
154
- newDate.setFullYear(newDate.getFullYear() + 1)
155
- } else if (view === 'week') {
156
- newDate.setDate(newDate.getDate() + 7)
157
- } else if (view === 'day') {
158
- newDate.setDate(newDate.getDate() + 1)
159
- }
160
  setCurrentDate(newDate)
161
  }
162
 
163
- const handleToday = () => {
164
- setCurrentDate(new Date())
165
- }
166
 
167
- // Helper for Week View
168
  const getWeekDays = () => {
169
  const startOfWeek = new Date(currentDate)
170
  const day = startOfWeek.getDay()
@@ -181,22 +163,15 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
181
  }
182
 
183
  const renderHeaderTitle = () => {
184
- if (view === 'year') {
185
- return <span className="font-bold">{currentDate.getFullYear()}</span>
186
- }
187
  if (view === 'day') {
188
  return (
189
  <div className="flex flex-col leading-tight">
190
- <span className="font-bold text-2xl">{currentDate.getDate()} {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
191
- <span className="text-lg font-normal text-gray-400">{weekDays[currentDate.getDay()]}</span>
192
  </div>
193
  )
194
  }
195
- return (
196
- <>
197
- <span className="font-bold">{monthNames[currentDate.getMonth()]}</span> {currentDate.getFullYear()}
198
- </>
199
- )
200
  }
201
 
202
  return (
@@ -213,320 +188,287 @@ export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: C
213
  height={750}
214
  x={50}
215
  y={50}
216
- className="calendar-window"
 
 
217
  >
218
- <div className="flex flex-col h-full bg-[#1E1E1E] text-white overflow-hidden">
219
- {/* Toolbar */}
220
- <div className="h-14 flex items-center justify-between px-4 border-b border-[#333] bg-[#252526]">
221
- <div className="flex items-center gap-4">
222
- <div className="flex items-center gap-2">
223
- <button
224
- onClick={handleToday}
225
- className="p-1.5 hover:bg-[#333] rounded-md text-gray-400 hover:text-white transition-colors"
226
- title="Go to Today"
227
- >
228
- <CalendarBlank size={18} />
229
- </button>
230
- </div>
231
- <div className="h-6 w-px bg-[#444]" />
232
- <button className="p-1.5 hover:bg-[#333] rounded-full text-gray-400 hover:text-white transition-colors">
233
- <Plus size={18} weight="bold" />
234
- </button>
235
- </div>
236
-
237
- <div className="flex items-center bg-[#111] rounded-lg p-1 border border-[#333]">
238
- {(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
239
- <button
240
- key={v}
241
- onClick={() => setView(v)}
242
- className={`px-3 py-1 text-xs font-medium rounded-md capitalize transition-all ${view === v
243
- ? 'bg-[#333] text-white shadow-sm'
244
- : 'text-gray-400 hover:text-gray-200'
245
- }`}
246
- >
247
- {v}
248
- </button>
249
- ))}
250
- </div>
251
-
252
- <div className="flex items-center gap-2">
253
- <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'}`}>
254
- <button onClick={() => setIsSearchOpen(!isSearchOpen)} className="text-gray-400 hover:text-white">
255
- <MagnifyingGlass size={16} />
256
- </button>
257
- {isSearchOpen && (
258
- <input
259
- type="text"
260
- placeholder="Search"
261
- className="bg-transparent border-none outline-none text-xs text-white ml-2 w-full"
262
- value={searchQuery}
263
- onChange={(e) => setSearchQuery(e.target.value)}
264
- autoFocus
265
- />
266
- )}
267
- </div>
268
- </div>
269
- </div>
270
-
271
- {/* Header */}
272
- <div className="h-20 flex items-center justify-between px-6 py-4 shrink-0">
273
- <h1 className="text-3xl font-light tracking-tight flex items-baseline gap-2">
274
- {renderHeaderTitle()}
275
- </h1>
276
-
277
- <div className="flex items-center gap-2">
278
- <button
279
- onClick={handlePrev}
280
- className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[#333] transition-colors"
281
  >
282
- <CaretLeft size={20} />
283
- </button>
284
- <button
285
- onClick={handleToday}
286
- className="px-3 py-1 bg-[#333] hover:bg-[#444] rounded-md text-sm font-medium transition-colors border border-[#444]"
287
- >
288
- Today
289
- </button>
290
- <button
291
- onClick={handleNext}
292
- className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[#333] transition-colors"
293
- >
294
- <CaretRight size={20} />
295
- </button>
296
- </div>
297
- </div>
 
 
 
 
 
 
 
 
298
 
299
- {/* Views */}
300
- <div className="flex-1 overflow-hidden flex flex-col relative">
301
-
302
- {/* YEAR VIEW */}
303
- {view === 'year' && (
304
- <div className="flex-1 overflow-y-auto p-6">
305
- <div className="grid grid-cols-4 gap-x-8 gap-y-8">
306
- {monthNames.map((month, monthIndex) => {
307
- const days = getDaysInMonth(currentDate.getFullYear(), monthIndex)
308
- return (
309
- <div key={month} className="flex flex-col gap-2">
310
- <h3 className="text-red-500 font-medium text-lg pl-1">{month}</h3>
311
- <div className="grid grid-cols-7 gap-y-2 text-center">
312
- {/* Weekday Headers */}
313
- {weekDays.map(d => (
314
- <div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
315
- ))}
316
- {/* Days */}
317
- {days.map((d, i) => {
318
- if (!d.isCurrentMonth) return <div key={i} />
319
- const dateStr = `${d.year}-${String(d.month + 1).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`
320
- const hasEvents = getEventsForDate(dateStr).length > 0
321
- const isCurrentDay = isToday(d.day, d.month, d.year)
322
-
323
- return (
324
- <div
325
- key={i}
326
- className={`
327
- text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer hover:bg-[#333] relative
328
- ${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-gray-300'}
329
- `}
330
- onClick={() => {
331
- setCurrentDate(new Date(d.year, d.month, d.day))
332
- setView('day')
333
- }}
334
- >
335
- {d.day}
336
- {hasEvents && !isCurrentDay && (
337
- <div className="absolute bottom-0.5 w-0.5 h-0.5 bg-gray-400 rounded-full" />
338
- )}
339
- </div>
340
- )
341
- })}
342
  </div>
343
- </div>
344
- )
345
- })}
346
  </div>
347
- </div>
348
  )}
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- {/* MONTH VIEW */}
351
- {view === 'month' && (
352
- <div className="flex-1 flex flex-col px-4 pb-4 overflow-hidden">
353
- <div className="grid grid-cols-7 mb-2 shrink-0">
354
- {weekDays.map(day => (
355
- <div key={day} className="text-right pr-2 text-sm font-medium text-gray-500">
356
- {day}
357
- </div>
358
- ))}
 
 
359
  </div>
360
 
361
- <div className="flex-1 grid grid-cols-7 grid-rows-6 border-t border-l border-[#333] bg-[#1E1E1E]">
362
- {calendarDays.map((dateObj, index) => {
363
- const dateStr = `${dateObj.year}-${String(dateObj.month + 1).padStart(2, '0')}-${String(dateObj.day).padStart(2, '0')}`
364
- const dayEvents = getEventsForDate(dateStr)
365
- const isCurrentDay = isToday(dateObj.day, dateObj.month, dateObj.year)
366
-
367
- return (
368
- <div
369
- key={index}
370
- className={`
371
- border-b border-r border-[#333] p-1 relative group transition-colors
372
- ${!dateObj.isCurrentMonth ? 'bg-[#1a1a1a] text-gray-600' : 'hover:bg-[#252526]'}
373
- `}
374
- onClick={() => {
375
- setCurrentDate(new Date(dateObj.year, dateObj.month, dateObj.day))
376
- setView('day')
377
- }}
378
- >
379
- <div className="flex justify-end mb-1">
380
- <span
381
- className={`
382
- text-sm font-medium w-7 h-7 flex items-center justify-center rounded-full
383
- ${isCurrentDay
384
- ? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
385
- : dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
386
- `}
387
- >
388
- {dateObj.day}
389
- </span>
390
- </div>
391
-
392
- <div className="space-y-1 overflow-y-auto max-h-[calc(100%-2rem)] custom-scrollbar">
393
- {dayEvents.map((event, i) => (
394
- <div
395
- key={i}
396
- className={`
397
- text-[10px] px-1.5 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
398
- ${event.color || 'bg-blue-500'} text-white font-medium shadow-sm
399
- ${event.id === 'reuben-bday' ? 'animate-pulse ring-1 ring-pink-300' : ''}
400
- `}
401
- title={event.title}
402
- >
403
- {event.title}
404
- </div>
405
- ))}
406
- </div>
407
- </div>
408
- )
409
- })}
410
  </div>
 
 
 
 
 
 
 
411
  </div>
412
- )}
413
 
414
- {/* WEEK VIEW */}
415
- {view === 'week' && (
416
- <div className="flex-1 flex flex-col overflow-hidden">
417
- {/* Week Header */}
418
- <div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 pb-2">
419
- {getWeekDays().map((d, i) => {
420
- const isCurrentDay = isToday(d.getDate(), d.getMonth(), d.getFullYear())
421
- return (
422
- <div key={i} className="flex-1 text-center border-l border-[#333] first:border-l-0">
423
- <div className={`text-xs font-medium mb-1 ${isCurrentDay ? 'text-red-500' : 'text-gray-500'}`}>
424
- {weekDays[d.getDay()]}
 
 
 
 
 
425
  </div>
426
- <div className={`
427
- text-xl font-light w-8 h-8 flex items-center justify-center rounded-full mx-auto
428
- ${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-white'}
429
- `}>
430
- {d.getDate()}
431
- </div>
432
- </div>
433
- )
434
- })}
435
- </div>
436
 
437
- {/* All Day Section */}
438
- <div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 py-1 min-h-[40px]">
439
- <div className="absolute left-2 text-[10px] text-gray-500 top-32">all-day</div>
440
- {getWeekDays().map((d, i) => {
441
- const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
442
- const dayEvents = getEventsForDate(dateStr)
443
- return (
444
- <div key={i} className="flex-1 px-1 border-l border-[#333] first:border-l-0 space-y-1">
445
- {dayEvents.map((event, idx) => (
446
  <div
447
- key={idx}
448
  className={`
449
- text-[10px] px-1.5 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
450
- ${event.color || 'bg-blue-500'} text-white font-medium
451
  `}
452
- title={event.title}
 
 
 
453
  >
454
- {event.title}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  </div>
456
- ))}
457
- </div>
458
- )
459
- })}
460
- </div>
461
 
462
- {/* Scrollable Time Grid */}
463
- <div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
464
- <div className="absolute top-0 left-0 w-full min-h-full">
465
- {hours.map((hour) => (
466
- <div key={hour} className="flex h-[60px] border-b border-[#333] group">
467
- {/* Time Label */}
468
- <div className="w-14 shrink-0 text-right pr-2 text-xs text-gray-500 -mt-2.5 bg-[#1E1E1E] z-10">
469
- {hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
470
- </div>
471
- {/* Columns */}
472
- <div className="flex-1 flex">
473
- {Array.from({ length: 7 }).map((_, colIndex) => (
474
- <div key={colIndex} className="flex-1 border-l border-[#333] first:border-l-0 relative group-hover:bg-[#252526]/30 transition-colors">
475
- {/* Time slots would go here */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
  </div>
477
- ))}
478
- </div>
479
- </div>
480
- ))}
481
- {/* Current Time Indicator (if in current week) */}
482
- {/* Simplified: just show if today is in view */}
483
- </div>
484
- </div>
485
- </div>
486
- )}
487
-
488
- {/* DAY VIEW */}
489
- {view === 'day' && (
490
- <div className="flex-1 flex flex-col overflow-hidden">
491
- {/* All Day Section */}
492
- <div className="flex border-b border-[#333] shrink-0 pl-14 pr-4 py-2 min-h-[50px]">
493
- <div className="w-14 shrink-0 text-[10px] text-gray-500 pt-1 pr-2 text-right">all-day</div>
494
- <div className="flex-1 px-1 space-y-1 border-l border-[#333]">
495
- {getEventsForDate(`${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}-${String(currentDate.getDate()).padStart(2, '0')}`).map((event, idx) => (
496
- <div
497
- key={idx}
498
- className={`
499
- text-xs px-2 py-1 rounded-sm truncate cursor-pointer hover:opacity-80
500
- ${event.color || 'bg-blue-500'} text-white font-medium
501
- `}
502
- title={event.title}
503
- >
504
- {event.title}
505
- </div>
506
- ))}
507
- </div>
508
- </div>
509
 
510
- {/* Scrollable Time Grid */}
511
- <div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative">
512
- <div className="absolute top-0 left-0 w-full min-h-full">
513
- {hours.map((hour) => (
514
- <div key={hour} className="flex h-[60px] border-b border-[#333]">
515
- {/* Time Label */}
516
- <div className="w-14 shrink-0 text-right pr-2 text-xs text-gray-500 -mt-2.5 bg-[#1E1E1E] z-10">
517
- {hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  </div>
519
- {/* Column */}
520
- <div className="flex-1 border-l border-[#333] relative hover:bg-[#252526]/30 transition-colors">
521
- {/* Time slots would go here */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  </div>
523
  </div>
524
- ))}
525
- </div>
526
- </div>
527
- </div>
528
- )}
529
-
530
  </div>
531
  </div>
532
  </Window>
 
6
  CaretRight,
7
  Plus,
8
  MagnifyingGlass,
 
9
  CalendarBlank,
10
+ List,
11
+ Check
12
  } from '@phosphor-icons/react'
13
  import { motion, AnimatePresence } from 'framer-motion'
14
  import { useKV } from '../hooks/useKV'
 
32
  export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: CalendarProps) {
33
  const [currentDate, setCurrentDate] = useState(new Date())
34
  const [view, setView] = useState<ViewMode>('month')
 
35
  const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
36
  const [searchQuery, setSearchQuery] = useState('')
37
  const [isSearchOpen, setIsSearchOpen] = useState(false)
38
  const scrollContainerRef = useRef<HTMLDivElement>(null)
39
+ const [sidebarOpen, setSidebarOpen] = useState(true)
40
 
41
  // Scroll to 8 AM on mount for day/week views
42
  useEffect(() => {
43
  if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
 
44
  scrollContainerRef.current.scrollTop = 8 * 60
45
  }
46
  }, [view])
47
 
48
+ // Generate all events
49
  const allEvents = useMemo(() => {
50
  const year = currentDate.getFullYear()
 
51
  const years = [year - 1, year, year + 1]
52
  let events: CalendarEvent[] = [...customEvents]
53
 
 
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()
79
 
 
80
  const prevMonthDays = []
81
  const prevMonthLastDay = new Date(year, month, 0).getDate()
82
  for (let i = startingDayOfWeek - 1; i >= 0; i--) {
 
88
  })
89
  }
90
 
 
91
  const currentMonthDays = []
92
  for (let i = 1; i <= daysInMonth; i++) {
93
  currentMonthDays.push({
 
98
  })
99
  }
100
 
 
101
  const nextMonthDays = []
102
  const totalDaysSoFar = prevMonthDays.length + currentMonthDays.length
103
+ const remainingCells = 42 - totalDaysSoFar
104
 
105
  for (let i = 1; i <= remainingCells; i++) {
106
  nextMonthDays.push({
 
129
 
130
  const handlePrev = () => {
131
  const newDate = new Date(currentDate)
132
+ if (view === 'month') newDate.setMonth(newDate.getMonth() - 1)
133
+ else if (view === 'year') newDate.setFullYear(newDate.getFullYear() - 1)
134
+ else if (view === 'week') newDate.setDate(newDate.getDate() - 7)
135
+ else if (view === 'day') newDate.setDate(newDate.getDate() - 1)
 
 
 
 
 
136
  setCurrentDate(newDate)
137
  }
138
 
139
  const handleNext = () => {
140
  const newDate = new Date(currentDate)
141
+ if (view === 'month') newDate.setMonth(newDate.getMonth() + 1)
142
+ else if (view === 'year') newDate.setFullYear(newDate.getFullYear() + 1)
143
+ else if (view === 'week') newDate.setDate(newDate.getDate() + 7)
144
+ else if (view === 'day') newDate.setDate(newDate.getDate() + 1)
 
 
 
 
 
145
  setCurrentDate(newDate)
146
  }
147
 
148
+ const handleToday = () => setCurrentDate(new Date())
 
 
149
 
 
150
  const getWeekDays = () => {
151
  const startOfWeek = new Date(currentDate)
152
  const day = startOfWeek.getDay()
 
163
  }
164
 
165
  const renderHeaderTitle = () => {
166
+ if (view === 'year') return <span className="font-semibold text-xl">{currentDate.getFullYear()}</span>
 
 
167
  if (view === 'day') {
168
  return (
169
  <div className="flex flex-col leading-tight">
170
+ <span className="font-semibold text-xl">{currentDate.getDate()} {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
 
171
  </div>
172
  )
173
  }
174
+ return <span className="font-semibold text-xl">{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
 
 
 
 
175
  }
176
 
177
  return (
 
188
  height={750}
189
  x={50}
190
  y={50}
191
+ className="calendar-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
192
+ contentClassName="!bg-transparent"
193
+ headerClassName="!bg-transparent border-b border-white/5"
194
  >
195
+ <div className="flex h-full text-white overflow-hidden">
196
+ {/* Sidebar */}
197
+ <AnimatePresence initial={false}>
198
+ {sidebarOpen && (
199
+ <motion.div
200
+ initial={{ width: 0, opacity: 0 }}
201
+ animate={{ width: 240, opacity: 1 }}
202
+ exit={{ width: 0, opacity: 0 }}
203
+ className="border-r border-white/10 bg-black/20 backdrop-blur-md flex flex-col"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  >
205
+ <div className="p-4">
206
+ <div className="grid grid-cols-7 gap-y-2 text-center mb-4">
207
+ {weekDays.map(d => (
208
+ <div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
209
+ ))}
210
+ {calendarDays.map((d, i) => {
211
+ if (!d.isCurrentMonth) return <div key={i} className="text-gray-600 text-xs">{d.day}</div>
212
+ const isCurrentDay = isToday(d.day, d.month, d.year)
213
+ const isSelected = d.day === currentDate.getDate() && d.month === currentDate.getMonth()
214
+
215
+ return (
216
+ <div
217
+ key={i}
218
+ onClick={() => setCurrentDate(new Date(d.year, d.month, d.day))}
219
+ className={`
220
+ text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer transition-colors
221
+ ${isCurrentDay ? 'bg-red-500 text-white font-bold' : isSelected ? 'bg-white/20 text-white' : 'text-gray-300 hover:bg-white/10'}
222
+ `}
223
+ >
224
+ {d.day}
225
+ </div>
226
+ )
227
+ })}
228
+ </div>
229
 
230
+ <div className="mt-8 space-y-4">
231
+ <div className="flex items-center justify-between text-xs text-gray-400 font-medium px-2">
232
+ <span>CALENDARS</span>
233
+ </div>
234
+ <div className="space-y-1">
235
+ {['Home', 'Work', 'Holidays', 'Birthdays'].map((cal, i) => (
236
+ <div key={cal} className="flex items-center gap-3 px-2 py-1.5 rounded-md hover:bg-white/5 cursor-pointer group">
237
+ <div className={`w-3 h-3 rounded-full border-2 ${i === 0 ? 'border-blue-500 bg-blue-500' :
238
+ i === 1 ? 'border-purple-500 bg-purple-500' :
239
+ i === 2 ? 'border-green-500 bg-green-500' :
240
+ 'border-red-500 bg-red-500'
241
+ }`} />
242
+ <span className="text-sm text-gray-300">{cal}</span>
243
+ <Check size={12} className="ml-auto opacity-0 group-hover:opacity-100 text-gray-500" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  </div>
245
+ ))}
246
+ </div>
247
+ </div>
248
  </div>
249
+ </motion.div>
250
  )}
251
+ </AnimatePresence>
252
+
253
+ {/* Main Content */}
254
+ <div className="flex-1 flex flex-col bg-transparent">
255
+ {/* Toolbar */}
256
+ <div className="h-14 flex items-center justify-between px-6 border-b border-white/10 bg-[#252525]/30 backdrop-blur-sm">
257
+ <div className="flex items-center gap-4">
258
+ <button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-2 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white">
259
+ <List size={20} />
260
+ </button>
261
+ <div className="text-2xl font-light tracking-tight">{renderHeaderTitle()}</div>
262
+ </div>
263
 
264
+ <div className="flex items-center gap-4">
265
+ <div className="flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
266
+ <button onClick={handlePrev} className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
267
+ <CaretLeft size={16} />
268
+ </button>
269
+ <button onClick={handleToday} className="px-3 py-1 text-sm font-medium text-gray-300 hover:text-white transition-colors">
270
+ Today
271
+ </button>
272
+ <button onClick={handleNext} className="p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
273
+ <CaretRight size={16} />
274
+ </button>
275
  </div>
276
 
277
+ <div className="flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
278
+ {(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
279
+ <button
280
+ key={v}
281
+ onClick={() => setView(v)}
282
+ className={`px-3 py-1 text-xs font-medium rounded-md capitalize transition-all ${view === v
283
+ ? 'bg-[#444] text-white shadow-sm'
284
+ : 'text-gray-400 hover:text-gray-200'
285
+ }`}
286
+ >
287
+ {v}
288
+ </button>
289
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  </div>
291
+
292
+ <button className="p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
293
+ <Plus size={20} />
294
+ </button>
295
+ <button className="p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
296
+ <MagnifyingGlass size={20} />
297
+ </button>
298
  </div>
299
+ </div>
300
 
301
+ {/* View Content */}
302
+ <div className="flex-1 overflow-hidden relative">
303
+ <AnimatePresence mode="wait">
304
+ {view === 'month' && (
305
+ <motion.div
306
+ key="month"
307
+ initial={{ opacity: 0, scale: 0.98 }}
308
+ animate={{ opacity: 1, scale: 1 }}
309
+ exit={{ opacity: 0, scale: 1.02 }}
310
+ transition={{ duration: 0.2 }}
311
+ className="h-full flex flex-col"
312
+ >
313
+ <div className="grid grid-cols-7 border-b border-white/5 bg-black/10">
314
+ {weekDays.map(day => (
315
+ <div key={day} className="py-2 text-right pr-4 text-xs font-medium text-gray-500 uppercase tracking-wider">
316
+ {day}
317
  </div>
318
+ ))}
319
+ </div>
320
+ <div className="flex-1 grid grid-cols-7 grid-rows-6">
321
+ {calendarDays.map((dateObj, index) => {
322
+ const dateStr = `${dateObj.year}-${String(dateObj.month + 1).padStart(2, '0')}-${String(dateObj.day).padStart(2, '0')}`
323
+ const dayEvents = getEventsForDate(dateStr)
324
+ const isCurrentDay = isToday(dateObj.day, dateObj.month, dateObj.year)
 
 
 
325
 
326
+ return (
 
 
 
 
 
 
 
 
327
  <div
328
+ key={index}
329
  className={`
330
+ border-b border-r border-white/5 p-1 relative group transition-colors
331
+ ${!dateObj.isCurrentMonth ? 'bg-black/20 text-gray-600' : 'hover:bg-white/5'}
332
  `}
333
+ onClick={() => {
334
+ setCurrentDate(new Date(dateObj.year, dateObj.month, dateObj.day))
335
+ setView('day')
336
+ }}
337
  >
338
+ <div className="flex justify-end mb-1">
339
+ <span
340
+ className={`
341
+ text-sm font-medium w-7 h-7 flex items-center justify-center rounded-full
342
+ ${isCurrentDay
343
+ ? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
344
+ : dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
345
+ `}
346
+ >
347
+ {dateObj.day}
348
+ </span>
349
+ </div>
350
+ <div className="space-y-1 overflow-y-auto max-h-[calc(100%-2rem)] [&::-webkit-scrollbar]:hidden">
351
+ {dayEvents.map((event, i) => (
352
+ <div
353
+ key={i}
354
+ className={`
355
+ text-[10px] px-1.5 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
356
+ ${event.color || 'bg-blue-500'} text-white font-medium shadow-sm backdrop-blur-sm bg-opacity-80
357
+ `}
358
+ >
359
+ {event.title}
360
+ </div>
361
+ ))}
362
+ </div>
363
  </div>
364
+ )
365
+ })}
366
+ </div>
367
+ </motion.div>
368
+ )}
369
 
370
+ {view === 'year' && (
371
+ <motion.div
372
+ key="year"
373
+ initial={{ opacity: 0 }}
374
+ animate={{ opacity: 1 }}
375
+ exit={{ opacity: 0 }}
376
+ className="h-full overflow-y-auto p-8 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2"
377
+ >
378
+ <div className="grid grid-cols-4 gap-x-8 gap-y-12">
379
+ {monthNames.map((month, monthIndex) => {
380
+ const days = getDaysInMonth(currentDate.getFullYear(), monthIndex)
381
+ return (
382
+ <div key={month} className="flex flex-col gap-4">
383
+ <h3 className="text-red-500 font-medium text-xl pl-1">{month}</h3>
384
+ <div className="grid grid-cols-7 gap-y-3 text-center">
385
+ {weekDays.map(d => (
386
+ <div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
387
+ ))}
388
+ {days.map((d, i) => {
389
+ if (!d.isCurrentMonth) return <div key={i} />
390
+ const isCurrentDay = isToday(d.day, d.month, d.year)
391
+ return (
392
+ <div
393
+ key={i}
394
+ onClick={() => {
395
+ setCurrentDate(new Date(d.year, d.month, d.day))
396
+ setView('month')
397
+ }}
398
+ className={`
399
+ text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer hover:bg-white/10
400
+ ${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-gray-300'}
401
+ `}
402
+ >
403
+ {d.day}
404
+ </div>
405
+ )
406
+ })}
407
  </div>
408
+ </div>
409
+ )
410
+ })}
411
+ </div>
412
+ </motion.div>
413
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ {(view === 'week' || view === 'day') && (
416
+ <motion.div
417
+ key="week-day"
418
+ initial={{ opacity: 0 }}
419
+ animate={{ opacity: 1 }}
420
+ exit={{ opacity: 0 }}
421
+ className="h-full flex flex-col"
422
+ >
423
+ <div className="flex border-b border-white/10 shrink-0 pl-14 pr-4 py-2">
424
+ {view === 'week' ? getWeekDays().map((d, i) => {
425
+ const isCurrentDay = isToday(d.getDate(), d.getMonth(), d.getFullYear())
426
+ return (
427
+ <div key={i} className="flex-1 text-center border-l border-white/5 first:border-l-0">
428
+ <div className={`text-xs font-medium mb-1 uppercase ${isCurrentDay ? 'text-red-500' : 'text-gray-500'}`}>
429
+ {weekDays[d.getDay()]}
430
+ </div>
431
+ <div className={`
432
+ text-xl font-light w-8 h-8 flex items-center justify-center rounded-full mx-auto
433
+ ${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-white'}
434
+ `}>
435
+ {d.getDate()}
436
+ </div>
437
+ </div>
438
+ )
439
+ }) : (
440
+ <div className="flex-1 px-4">
441
+ <div className="text-red-500 font-medium uppercase text-sm">{weekDays[currentDate.getDay()]}</div>
442
+ <div className="text-3xl font-light">{currentDate.getDate()}</div>
443
  </div>
444
+ )}
445
+ </div>
446
+
447
+ <div ref={scrollContainerRef} className="flex-1 overflow-y-auto relative [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
448
+ <div className="absolute top-0 left-0 w-full min-h-full bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjYwIj48bGluZSB4MT0iMCIgeTE9IjYwIiB4Mj0iMTAwJSIgeTI9IjYwIiBzdHJva2U9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiIHN0cm9rZS13aWR0aD0iMSIvPjwvc3ZnPg==')]">
449
+ {hours.map((hour) => (
450
+ <div key={hour} className="flex h-[60px] group">
451
+ <div className="w-14 shrink-0 text-right pr-3 text-xs text-gray-500 -mt-2.5 z-10">
452
+ {hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
453
+ </div>
454
+ <div className="flex-1 border-l border-white/5 relative group-hover:bg-white/[0.02] transition-colors">
455
+ {/* Time slots */}
456
+ </div>
457
+ </div>
458
+ ))}
459
+ {/* Current Time Indicator */}
460
+ <div
461
+ className="absolute left-14 right-0 border-t border-red-500 z-20 pointer-events-none"
462
+ style={{ top: `${(new Date().getHours() * 60 + new Date().getMinutes())}px` }}
463
+ >
464
+ <div className="absolute -left-1.5 -top-1.5 w-3 h-3 rounded-full bg-red-500" />
465
  </div>
466
  </div>
467
+ </div>
468
+ </motion.div>
469
+ )}
470
+ </AnimatePresence>
471
+ </div>
 
472
  </div>
473
  </div>
474
  </Window>
app/components/Clock.tsx CHANGED
@@ -1,7 +1,9 @@
1
  'use client'
2
 
3
- import React, { useState, useEffect } from 'react'
 
4
  import Window from './Window'
 
5
 
6
  interface ClockProps {
7
  onClose: () => void
@@ -11,183 +13,344 @@ interface ClockProps {
11
  zIndex?: number
12
  }
13
 
14
- export function Clock({ onClose, onMinimize, onMaximize, onFocus, zIndex }: ClockProps) {
 
15
  const [time, setTime] = useState(new Date())
16
- const [viewMode, setViewMode] = useState<'analog' | 'digital' | 'world'>('analog')
17
 
18
  useEffect(() => {
19
- const timer = setInterval(() => {
 
20
  setTime(new Date())
21
- }, 1000)
22
-
23
- return () => clearInterval(timer)
 
24
  }, [])
25
 
26
- // Calculate rotation angles for clock hands
27
- const seconds = time.getSeconds()
28
- const minutes = time.getMinutes()
29
- const hours = time.getHours()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- const secondDegrees = (seconds / 60) * 360 - 90
32
- const minuteDegrees = ((minutes / 60) * 360) + ((seconds / 60) * 6) - 90
33
- const hourDegrees = ((hours % 12 / 12) * 360) + ((minutes / 60) * 30) - 90
 
 
34
 
35
- const worldClocks = [
 
36
  { city: 'New York', offset: -5 },
37
  { city: 'London', offset: 0 },
38
  { city: 'Tokyo', offset: 9 },
39
- { city: 'Sydney', offset: 11 }
 
 
 
 
 
40
  ]
41
 
42
- const getWorldTime = (offset: number) => {
43
- const utc = time.getTime() + (time.getTimezoneOffset() * 60000)
44
- const cityTime = new Date(utc + (3600000 * offset))
45
- return cityTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
 
 
 
 
 
46
  }
47
 
48
  return (
49
- <Window
50
- id="clock"
51
- title="Clock"
52
- isOpen={true}
53
- onClose={onClose}
54
- onMinimize={onMinimize}
55
- onMaximize={onMaximize}
56
- onFocus={onFocus}
57
- zIndex={zIndex}
58
- width={380}
59
- height={480}
60
- x={window.innerWidth / 2 - 190}
61
- y={window.innerHeight / 2 - 240}
62
- darkMode={true}
63
- className="clock-app-window"
64
- >
65
- <div className="flex flex-col h-full bg-gradient-to-b from-gray-900 via-gray-800 to-black">
66
- {/* Tab Bar */}
67
- <div className="flex border-b border-gray-700 bg-gray-900/50">
68
- <button
69
- onClick={() => setViewMode('analog')}
70
- className={`flex-1 py-3 text-sm font-medium transition-all ${viewMode === 'analog'
71
- ? 'text-orange-400 border-b-2 border-orange-400'
72
- : 'text-gray-400 hover:text-white'
73
- }`}
74
- >
75
- Analog
76
- </button>
77
- <button
78
- onClick={() => setViewMode('digital')}
79
- className={`flex-1 py-3 text-sm font-medium transition-all ${viewMode === 'digital'
80
- ? 'text-orange-400 border-b-2 border-orange-400'
81
- : 'text-gray-400 hover:text-white'
82
- }`}
83
- >
84
- Digital
85
- </button>
86
- <button
87
- onClick={() => setViewMode('world')}
88
- className={`flex-1 py-3 text-sm font-medium transition-all ${viewMode === 'world'
89
- ? 'text-orange-400 border-b-2 border-orange-400'
90
- : 'text-gray-400 hover:text-white'
91
- }`}
92
- >
93
- World Clock
94
- </button>
95
  </div>
 
96
 
97
- {/* Clock Display */}
98
- <div className="flex-1 flex items-center justify-center p-6">
99
- {viewMode === 'analog' && (
100
- <div className="flex flex-col items-center">
101
- <div className="relative w-64 h-64">
102
- {/* Clock Face */}
103
- <div className="absolute inset-0 rounded-full bg-gradient-to-br from-gray-800 to-black shadow-inner border-4 border-gray-700">
104
- {/* Hour Markers */}
105
- {[...Array(12)].map((_, i) => (
106
- <div key={i}>
107
- <div
108
- className="absolute bg-white"
109
- style={{
110
- width: i % 3 === 0 ? '3px' : '1px',
111
- height: i % 3 === 0 ? '12px' : '8px',
112
- top: '10px',
113
- left: '50%',
114
- transform: `translateX(-50%) rotate(${i * 30}deg)`,
115
- transformOrigin: '50% 118px'
116
- }}
117
- />
118
- </div>
119
- ))}
120
-
121
- {/* Clock Hands */}
122
- <div
123
- className="absolute top-1/2 left-1/2 w-2 h-20 bg-white rounded-full origin-bottom shadow-lg"
124
- style={{
125
- transform: `translate(-50%, -100%) rotate(${hourDegrees}deg)`
126
- }}
127
- />
128
- <div
129
- className="absolute top-1/2 left-1/2 w-1.5 h-28 bg-gray-300 rounded-full origin-bottom shadow-lg"
130
- style={{
131
- transform: `translate(-50%, -100%) rotate(${minuteDegrees}deg)`
132
- }}
133
  />
134
- <div
135
- className="absolute top-1/2 left-1/2 w-0.5 h-32 bg-orange-500 rounded-full origin-bottom"
136
- style={{
137
- transform: `translate(-50%, -100%) rotate(${secondDegrees}deg)`
138
- }}
139
- />
140
-
141
- {/* Center Dot */}
142
- <div className="absolute top-1/2 left-1/2 w-4 h-4 bg-orange-500 rounded-full transform -translate-x-1/2 -translate-y-1/2 shadow-lg z-10" />
143
  </div>
144
- </div>
145
-
146
- {/* Digital Time Below Analog Clock */}
147
- <div className="mt-6 text-center">
148
- <div className="text-2xl font-mono text-gray-300">
149
- {time.toLocaleTimeString()}
150
- </div>
151
- <div className="text-sm text-gray-500 mt-2">
152
- {time.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
153
- </div>
154
- </div>
155
- </div>
156
  )}
157
 
158
- {viewMode === 'digital' && (
159
- <div className="text-center">
160
- <div className="text-7xl font-mono font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-orange-600">
161
- {time.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  </div>
163
- <div className="mt-8 text-2xl text-gray-400">
164
- {time.toLocaleDateString('en-US', { weekday: 'long' })}
165
- </div>
166
- <div className="text-lg text-gray-500">
167
- {time.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
168
- </div>
169
- </div>
170
- )}
171
 
172
- {viewMode === 'world' && (
173
- <div className="w-full max-w-sm">
174
- <div className="space-y-4">
175
- {worldClocks.map((clock) => (
176
- <div key={clock.city} className="bg-gray-800 rounded-lg p-4 flex justify-between items-center">
 
 
177
  <div>
178
- <div className="text-white font-medium">{clock.city}</div>
179
- <div className="text-xs text-gray-400">UTC{clock.offset >= 0 ? '+' : ''}{clock.offset}</div>
180
  </div>
181
- <div className="text-2xl font-mono text-orange-400">
182
- {getWorldTime(clock.offset)}
183
  </div>
184
- </div>
185
  ))}
 
 
 
 
 
186
  </div>
187
- </div>
188
  )}
189
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </Window>
192
  )
193
  }
 
1
  'use client'
2
 
3
+ import React, { useState, useEffect, useRef } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
  import Window from './Window'
6
+ import { Globe, Clock as ClockIcon, Type, Plus } from 'lucide-react'
7
 
8
  interface ClockProps {
9
  onClose: () => void
 
13
  zIndex?: number
14
  }
15
 
16
+ // Helper to get smooth time
17
+ const useSmoothTime = () => {
18
  const [time, setTime] = useState(new Date())
 
19
 
20
  useEffect(() => {
21
+ let frameId: number
22
+ const update = () => {
23
  setTime(new Date())
24
+ frameId = requestAnimationFrame(update)
25
+ }
26
+ update()
27
+ return () => cancelAnimationFrame(frameId)
28
  }, [])
29
 
30
+ return time
31
+ }
32
+
33
+ const AnalogFace = ({ time, city, offset, isSmall = false }: { time: Date, city?: string, offset?: number, isSmall?: boolean }) => {
34
+ // Calculate time with offset if provided
35
+ const displayTime = offset !== undefined
36
+ ? new Date(time.getTime() + (time.getTimezoneOffset() * 60000) + (3600000 * offset))
37
+ : time
38
+
39
+ const seconds = displayTime.getSeconds() + displayTime.getMilliseconds() / 1000
40
+ const minutes = displayTime.getMinutes() + seconds / 60
41
+ const hours = displayTime.getHours() % 12 + minutes / 60
42
+
43
+ return (
44
+ <div className={`relative flex flex-col items-center ${isSmall ? 'gap-2' : 'gap-8'}`}>
45
+ <div className={`relative ${isSmall ? 'w-32 h-32' : 'w-64 h-64'} rounded-full bg-[#1e1e1e] shadow-[inset_0_0_20px_rgba(0,0,0,0.5)] border-[3px] border-[#333]`}>
46
+ {/* Clock Face Markings */}
47
+ {[...Array(12)].map((_, i) => (
48
+ <div
49
+ key={i}
50
+ className="absolute w-full h-full left-0 top-0"
51
+ style={{ transform: `rotate(${i * 30}deg)` }}
52
+ >
53
+ <div className={`absolute left-1/2 -translate-x-1/2 top-2 bg-gray-400 ${isSmall ? 'w-0.5 h-2' : 'w-1 h-3'
54
+ }`} />
55
+ {!isSmall && (
56
+ <span
57
+ className="absolute left-1/2 -translate-x-1/2 top-6 text-xl font-medium text-gray-300"
58
+ style={{ transform: `rotate(-${i * 30}deg)` }}
59
+ >
60
+ {i === 0 ? 12 : i}
61
+ </span>
62
+ )}
63
+ </div>
64
+ ))}
65
+
66
+ {/* Hands Container */}
67
+ <div className="absolute inset-0">
68
+ {/* Hour Hand */}
69
+ <div
70
+ className="absolute left-1/2 top-1/2 bg-white rounded-full origin-bottom shadow-lg z-10"
71
+ style={{
72
+ width: isSmall ? '3px' : '6px',
73
+ height: isSmall ? '25%' : '25%',
74
+ transform: `translate(-50%, -100%) rotate(${hours * 30}deg)`,
75
+ }}
76
+ />
77
+
78
+ {/* Minute Hand */}
79
+ <div
80
+ className="absolute left-1/2 top-1/2 bg-white rounded-full origin-bottom shadow-lg z-10"
81
+ style={{
82
+ width: isSmall ? '2px' : '4px',
83
+ height: isSmall ? '35%' : '38%',
84
+ transform: `translate(-50%, -100%) rotate(${minutes * 6}deg)`,
85
+ }}
86
+ />
87
+
88
+ {/* Second Hand (Orange) */}
89
+ <div
90
+ className="absolute left-1/2 top-1/2 bg-orange-500 rounded-full origin-bottom z-20"
91
+ style={{
92
+ width: isSmall ? '1px' : '2px',
93
+ height: isSmall ? '40%' : '45%',
94
+ transform: `translate(-50%, -100%) rotate(${seconds * 6}deg)`,
95
+ }}
96
+ >
97
+ {/* Tail of second hand */}
98
+ <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-[20%] w-full h-[20%] bg-orange-500" />
99
+ </div>
100
+
101
+ {/* Center Cap */}
102
+ <div className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black border-2 border-orange-500 z-30 ${isSmall ? 'w-1.5 h-1.5' : 'w-3 h-3'
103
+ }`} />
104
+ </div>
105
+ </div>
106
+
107
+ {/* Labels */}
108
+ <div className="text-center">
109
+ <div className={`${isSmall ? 'text-sm' : 'text-2xl'} font-medium text-white`}>
110
+ {city || 'Local Time'}
111
+ </div>
112
+ <div className={`${isSmall ? 'text-xs' : 'text-lg'} text-gray-400 font-mono mt-1`}>
113
+ {offset !== undefined
114
+ ? (offset === 0 ? 'Today' : `${offset > 0 ? '+' : ''}${offset}HRS`)
115
+ : displayTime.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })
116
+ }
117
+ </div>
118
+ {!isSmall && (
119
+ <div className="text-4xl font-light text-white mt-4 tracking-wider">
120
+ {displayTime.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false })}
121
+ </div>
122
+ )}
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ // List of available cities with offsets
129
+ const AVAILABLE_CITIES = [
130
+ { city: 'San Francisco', offset: -8, region: 'USA' },
131
+ { city: 'New York', offset: -5, region: 'USA' },
132
+ { city: 'London', offset: 0, region: 'UK' },
133
+ { city: 'Paris', offset: 1, region: 'Europe' },
134
+ { city: 'Berlin', offset: 1, region: 'Europe' },
135
+ { city: 'Moscow', offset: 3, region: 'Russia' },
136
+ { city: 'Dubai', offset: 4, region: 'UAE' },
137
+ { city: 'Mumbai', offset: 5.5, region: 'India' },
138
+ { city: 'Singapore', offset: 8, region: 'Asia' },
139
+ { city: 'Tokyo', offset: 9, region: 'Japan' },
140
+ { city: 'Sydney', offset: 11, region: 'Australia' },
141
+ { city: 'Auckland', offset: 13, region: 'New Zealand' },
142
+ { city: 'Honolulu', offset: -10, region: 'USA' },
143
+ { city: 'Rio de Janeiro', offset: -3, region: 'Brazil' },
144
+ ]
145
 
146
+ const ClockContent = () => {
147
+ const time = useSmoothTime()
148
+ const [activeTab, setActiveTab] = useState<'world' | 'analog' | 'digital'>('world')
149
+ const [showAddCity, setShowAddCity] = useState(false)
150
+ const [searchQuery, setSearchQuery] = useState('')
151
 
152
+ const [worldClocks, setWorldClocks] = useState([
153
+ { city: 'Cupertino', offset: -8 },
154
  { city: 'New York', offset: -5 },
155
  { city: 'London', offset: 0 },
156
  { city: 'Tokyo', offset: 9 },
157
+ ])
158
+
159
+ const tabs = [
160
+ { id: 'world', label: 'World Clock', icon: Globe },
161
+ { id: 'analog', label: 'Analog', icon: ClockIcon },
162
+ { id: 'digital', label: 'Digital', icon: Type },
163
  ]
164
 
165
+ const filteredCities = AVAILABLE_CITIES.filter(c =>
166
+ c.city.toLowerCase().includes(searchQuery.toLowerCase()) &&
167
+ !worldClocks.some(wc => wc.city === c.city)
168
+ )
169
+
170
+ const addCity = (city: typeof AVAILABLE_CITIES[0]) => {
171
+ setWorldClocks([...worldClocks, { city: city.city, offset: city.offset }])
172
+ setShowAddCity(false)
173
+ setSearchQuery('')
174
  }
175
 
176
  return (
177
+ <div className="flex flex-col h-full text-white relative">
178
+ {/* macOS-style Toolbar */}
179
+ <div className="flex items-center justify-center pt-4 pb-6 border-b border-white/10 bg-[#252525]/50">
180
+ <div className="flex bg-black/20 p-1 rounded-lg backdrop-blur-md">
181
+ {tabs.map((tab) => {
182
+ const Icon = tab.icon
183
+ const isActive = activeTab === tab.id
184
+ return (
185
+ <button
186
+ key={tab.id}
187
+ onClick={() => setActiveTab(tab.id as any)}
188
+ className={`relative px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200 flex items-center gap-2 ${isActive ? 'text-white' : 'text-gray-400 hover:text-gray-200'
189
+ }`}
190
+ >
191
+ {isActive && (
192
+ <motion.div
193
+ layoutId="activeTab"
194
+ className="absolute inset-0 bg-[#3a3a3a] rounded-md shadow-sm"
195
+ transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
196
+ />
197
+ )}
198
+ <span className="relative z-10 flex items-center gap-2">
199
+ <Icon size={14} />
200
+ {tab.label}
201
+ </span>
202
+ </button>
203
+ )
204
+ })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </div>
206
+ </div>
207
 
208
+ {/* Content Area */}
209
+ <div className="flex-1 overflow-y-auto p-8 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
210
+ <AnimatePresence mode="wait">
211
+ {activeTab === 'world' && !showAddCity && (
212
+ <motion.div
213
+ key="world"
214
+ initial={{ opacity: 0, y: 10 }}
215
+ animate={{ opacity: 1, y: 0 }}
216
+ exit={{ opacity: 0, y: -10 }}
217
+ className="grid grid-cols-2 gap-8 justify-items-center"
218
+ >
219
+ {worldClocks.map((clock) => (
220
+ <div key={clock.city} className="relative group">
221
+ <AnalogFace
222
+ time={time}
223
+ city={clock.city}
224
+ offset={clock.offset}
225
+ isSmall={true}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  />
227
+ <button
228
+ onClick={() => setWorldClocks(worldClocks.filter(c => c.city !== clock.city))}
229
+ className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg"
230
+ >
231
+ <Plus size={12} className="rotate-45" />
232
+ </button>
 
 
 
233
  </div>
234
+ ))}
235
+ {/* Add Button */}
236
+ <button
237
+ onClick={() => setShowAddCity(true)}
238
+ className="w-32 h-32 rounded-full border-2 border-dashed border-gray-600 flex flex-col items-center justify-center text-gray-500 hover:text-orange-500 hover:border-orange-500 transition-colors group"
239
+ >
240
+ <Plus size={32} className="group-hover:scale-110 transition-transform" />
241
+ <span className="text-xs mt-2 font-medium">Add City</span>
242
+ </button>
243
+ </motion.div>
 
 
244
  )}
245
 
246
+ {activeTab === 'world' && showAddCity && (
247
+ <motion.div
248
+ key="add-city"
249
+ initial={{ opacity: 0, scale: 0.95 }}
250
+ animate={{ opacity: 1, scale: 1 }}
251
+ exit={{ opacity: 0, scale: 0.95 }}
252
+ className="flex flex-col h-full max-w-md mx-auto"
253
+ >
254
+ <div className="flex items-center gap-2 mb-4">
255
+ <button
256
+ onClick={() => setShowAddCity(false)}
257
+ className="p-2 hover:bg-white/10 rounded-full transition-colors"
258
+ >
259
+ <Plus size={20} className="rotate-45" />
260
+ </button>
261
+ <input
262
+ type="text"
263
+ placeholder="Search city..."
264
+ value={searchQuery}
265
+ onChange={(e) => setSearchQuery(e.target.value)}
266
+ className="flex-1 bg-white/10 border border-white/20 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:border-orange-500 transition-colors"
267
+ autoFocus
268
+ />
269
  </div>
 
 
 
 
 
 
 
 
270
 
271
+ <div className="flex-1 overflow-y-auto space-y-2 pr-2 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
272
+ {filteredCities.map((city) => (
273
+ <button
274
+ key={city.city}
275
+ onClick={() => addCity(city)}
276
+ className="w-full flex items-center justify-between p-3 rounded-lg hover:bg-white/10 transition-colors group text-left"
277
+ >
278
  <div>
279
+ <div className="font-medium text-white">{city.city}</div>
280
+ <div className="text-xs text-gray-400">{city.region}</div>
281
  </div>
282
+ <div className="text-sm font-mono text-gray-500 group-hover:text-orange-400">
283
+ UTC{city.offset >= 0 ? '+' : ''}{city.offset}
284
  </div>
285
+ </button>
286
  ))}
287
+ {filteredCities.length === 0 && (
288
+ <div className="text-center text-gray-500 mt-8">
289
+ No cities found
290
+ </div>
291
+ )}
292
  </div>
293
+ </motion.div>
294
  )}
295
+
296
+ {activeTab === 'analog' && (
297
+ <motion.div
298
+ key="analog"
299
+ initial={{ opacity: 0, scale: 0.95 }}
300
+ animate={{ opacity: 1, scale: 1 }}
301
+ exit={{ opacity: 0, scale: 1.05 }}
302
+ className="flex items-center justify-center h-full"
303
+ >
304
+ <AnalogFace time={time} />
305
+ </motion.div>
306
+ )}
307
+
308
+ {activeTab === 'digital' && (
309
+ <motion.div
310
+ key="digital"
311
+ initial={{ opacity: 0 }}
312
+ animate={{ opacity: 1 }}
313
+ exit={{ opacity: 0 }}
314
+ className="flex flex-col items-center justify-center h-full"
315
+ >
316
+ <div className="text-[8rem] leading-none font-light tracking-tighter text-white tabular-nums">
317
+ {time.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' })}
318
+ </div>
319
+ <div className="text-4xl font-light text-orange-500 mt-4 tabular-nums">
320
+ {time.getSeconds().toString().padStart(2, '0')}
321
+ </div>
322
+ <div className="text-xl text-gray-400 mt-8 font-medium tracking-wide uppercase">
323
+ {time.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
324
+ </div>
325
+ </motion.div>
326
+ )}
327
+ </AnimatePresence>
328
  </div>
329
+ </div>
330
+ )
331
+ }
332
+
333
+ export function Clock({ onClose, onMinimize, onMaximize, onFocus, zIndex }: ClockProps) {
334
+ return (
335
+ <Window
336
+ id="clock"
337
+ title="Clock"
338
+ isOpen={true}
339
+ onClose={onClose}
340
+ onMinimize={onMinimize}
341
+ onMaximize={onMaximize}
342
+ onFocus={onFocus}
343
+ zIndex={zIndex}
344
+ width={600}
345
+ height={500}
346
+ x={window.innerWidth / 2 - 300}
347
+ y={window.innerHeight / 2 - 250}
348
+ darkMode={true}
349
+ className="clock-app-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
350
+ contentClassName="!bg-transparent"
351
+ headerClassName="!bg-transparent border-b border-white/5"
352
+ >
353
+ <ClockContent />
354
  </Window>
355
  )
356
  }
app/components/Desktop.tsx CHANGED
@@ -6,6 +6,7 @@ import { TopBar } from './TopBar'
6
  import { FileManager } from './FileManager'
7
  import { Calendar } from './Calendar'
8
  import { DraggableDesktopIcon } from './DraggableDesktopIcon'
 
9
 
10
  import { HelpModal } from './HelpModal'
11
  import { DesktopContextMenu } from './DesktopContextMenu'
@@ -38,13 +39,15 @@ import {
38
  FileText,
39
  DeviceMobile,
40
  Lightning,
41
- Function
 
42
  } from '@phosphor-icons/react'
43
 
44
  export function Desktop() {
45
  const [fileManagerOpen, setFileManagerOpen] = useState(false)
46
  const [calendarOpen, setCalendarOpen] = useState(false)
47
  const [clockOpen, setClockOpen] = useState(false)
 
48
 
49
  const [geminiChatOpen, setGeminiChatOpen] = useState(false)
50
 
@@ -73,6 +76,7 @@ export function Desktop() {
73
  const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
74
  const [calendarMinimized, setCalendarMinimized] = useState(false)
75
  const [clockMinimized, setClockMinimized] = useState(false)
 
76
 
77
  const [geminiChatMinimized, setGeminiChatMinimized] = useState(false)
78
 
@@ -102,6 +106,7 @@ export function Desktop() {
102
  setFileManagerOpen(false)
103
  setCalendarOpen(false)
104
  setClockOpen(false)
 
105
  setGeminiChatOpen(false)
106
  setFlutterRunnerOpen(false)
107
 
@@ -113,6 +118,7 @@ export function Desktop() {
113
  setFileManagerMinimized(false)
114
  setCalendarMinimized(false)
115
  setClockMinimized(false)
 
116
  setGeminiChatMinimized(false)
117
  setFlutterRunnerMinimized(false)
118
 
@@ -161,7 +167,16 @@ export function Desktop() {
161
  setClockMinimized(false)
162
  }
163
 
 
 
 
 
 
164
 
 
 
 
 
165
 
166
  const openGeminiChat = () => {
167
  console.log('Opening Gemini Chat')
@@ -254,7 +269,9 @@ export function Desktop() {
254
  case 'clock':
255
  openClock()
256
  break
257
-
 
 
258
  case 'gemini':
259
  openGeminiChat()
260
  break
@@ -413,7 +430,19 @@ export function Desktop() {
413
  })
414
  }
415
 
416
-
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
  if (geminiChatMinimized && geminiChatOpen) {
419
  minimizedApps.push({
@@ -505,13 +534,15 @@ export function Desktop() {
505
  fileManagerOpen,
506
  calendarOpen,
507
  clockOpen,
 
508
  geminiChatOpen,
509
  fileManagerMinimized,
510
  calendarMinimized,
511
  clockMinimized,
 
512
  geminiChatMinimized
513
  })
514
- }, [fileManagerOpen, calendarOpen, clockOpen, geminiChatOpen, fileManagerMinimized, calendarMinimized, clockMinimized, geminiChatMinimized])
515
 
516
  return (
517
  <div className="relative h-screen w-screen overflow-hidden flex touch-auto" onContextMenu={handleDesktopRightClick}>
@@ -549,12 +580,14 @@ export function Desktop() {
549
  onOpenFileManager={openFileManager}
550
  onOpenCalendar={openCalendar}
551
  onOpenClock={openClock}
 
552
  onOpenGeminiChat={openGeminiChat}
553
  onCloseAllApps={closeAllApps}
554
  openApps={{
555
  files: fileManagerOpen,
556
  calendar: calendarOpen,
557
  clock: clockOpen,
 
558
  gemini: geminiChatOpen
559
  }}
560
  minimizedApps={minimizedApps}
@@ -574,6 +607,17 @@ export function Desktop() {
574
  />
575
  </div>
576
 
 
 
 
 
 
 
 
 
 
 
 
577
  <div className="pointer-events-auto w-24 h-24">
578
  <DraggableDesktopIcon
579
  id="gemini"
@@ -617,10 +661,6 @@ export function Desktop() {
617
  />
618
  </div>
619
 
620
-
621
-
622
-
623
-
624
  <div className="pointer-events-auto w-24 h-24">
625
  <DraggableDesktopIcon
626
  id="flutter-editor"
@@ -737,7 +777,30 @@ export function Desktop() {
737
  </motion.div>
738
  )}
739
 
740
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
 
742
  {geminiChatOpen && (
743
  <motion.div
@@ -791,8 +854,6 @@ export function Desktop() {
791
  </motion.div>
792
  )}
793
 
794
-
795
-
796
  {flutterCodeEditorOpen && (
797
  <motion.div
798
  key="flutter-code-editor"
@@ -886,8 +947,6 @@ export function Desktop() {
886
  onAction={handleContextMenuAction}
887
  />
888
 
889
-
890
-
891
  {/* Help Modal */}
892
  <HelpModal isOpen={helpModalOpen} onClose={closeHelpModal} />
893
 
 
6
  import { FileManager } from './FileManager'
7
  import { Calendar } from './Calendar'
8
  import { DraggableDesktopIcon } from './DraggableDesktopIcon'
9
+ import { Messages } from './Messages'
10
 
11
  import { HelpModal } from './HelpModal'
12
  import { DesktopContextMenu } from './DesktopContextMenu'
 
39
  FileText,
40
  DeviceMobile,
41
  Lightning,
42
+ Function,
43
+ ChatCircleDots
44
  } from '@phosphor-icons/react'
45
 
46
  export function Desktop() {
47
  const [fileManagerOpen, setFileManagerOpen] = useState(false)
48
  const [calendarOpen, setCalendarOpen] = useState(false)
49
  const [clockOpen, setClockOpen] = useState(false)
50
+ const [messagesOpen, setMessagesOpen] = useState(false)
51
 
52
  const [geminiChatOpen, setGeminiChatOpen] = useState(false)
53
 
 
76
  const [fileManagerMinimized, setFileManagerMinimized] = useState(false)
77
  const [calendarMinimized, setCalendarMinimized] = useState(false)
78
  const [clockMinimized, setClockMinimized] = useState(false)
79
+ const [messagesMinimized, setMessagesMinimized] = useState(false)
80
 
81
  const [geminiChatMinimized, setGeminiChatMinimized] = useState(false)
82
 
 
106
  setFileManagerOpen(false)
107
  setCalendarOpen(false)
108
  setClockOpen(false)
109
+ setMessagesOpen(false)
110
  setGeminiChatOpen(false)
111
  setFlutterRunnerOpen(false)
112
 
 
118
  setFileManagerMinimized(false)
119
  setCalendarMinimized(false)
120
  setClockMinimized(false)
121
+ setMessagesMinimized(false)
122
  setGeminiChatMinimized(false)
123
  setFlutterRunnerMinimized(false)
124
 
 
167
  setClockMinimized(false)
168
  }
169
 
170
+ const openMessages = () => {
171
+ setMessagesOpen(true)
172
+ setMessagesMinimized(false)
173
+ setWindowZIndices(prev => ({ ...prev, messages: getNextZIndex() }))
174
+ }
175
 
176
+ const closeMessages = () => {
177
+ setMessagesOpen(false)
178
+ setMessagesMinimized(false)
179
+ }
180
 
181
  const openGeminiChat = () => {
182
  console.log('Opening Gemini Chat')
 
269
  case 'clock':
270
  openClock()
271
  break
272
+ case 'messages':
273
+ openMessages()
274
+ break
275
  case 'gemini':
276
  openGeminiChat()
277
  break
 
430
  })
431
  }
432
 
433
+ if (messagesMinimized && messagesOpen) {
434
+ minimizedApps.push({
435
+ id: 'messages',
436
+ label: 'Messages',
437
+ icon: (
438
+ <div className="bg-gradient-to-b from-[#4CD964] to-[#2E8B57] w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden">
439
+ <div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-50" />
440
+ <ChatCircleDots size={20} weight="fill" className="text-white relative z-10 drop-shadow-md" />
441
+ </div>
442
+ ),
443
+ onRestore: () => setMessagesMinimized(false)
444
+ })
445
+ }
446
 
447
  if (geminiChatMinimized && geminiChatOpen) {
448
  minimizedApps.push({
 
534
  fileManagerOpen,
535
  calendarOpen,
536
  clockOpen,
537
+ messagesOpen,
538
  geminiChatOpen,
539
  fileManagerMinimized,
540
  calendarMinimized,
541
  clockMinimized,
542
+ messagesMinimized,
543
  geminiChatMinimized
544
  })
545
+ }, [fileManagerOpen, calendarOpen, clockOpen, messagesOpen, geminiChatOpen, fileManagerMinimized, calendarMinimized, clockMinimized, messagesMinimized, geminiChatMinimized])
546
 
547
  return (
548
  <div className="relative h-screen w-screen overflow-hidden flex touch-auto" onContextMenu={handleDesktopRightClick}>
 
580
  onOpenFileManager={openFileManager}
581
  onOpenCalendar={openCalendar}
582
  onOpenClock={openClock}
583
+ onOpenMessages={openMessages}
584
  onOpenGeminiChat={openGeminiChat}
585
  onCloseAllApps={closeAllApps}
586
  openApps={{
587
  files: fileManagerOpen,
588
  calendar: calendarOpen,
589
  clock: clockOpen,
590
+ messages: messagesOpen,
591
  gemini: geminiChatOpen
592
  }}
593
  minimizedApps={minimizedApps}
 
607
  />
608
  </div>
609
 
610
+ <div className="pointer-events-auto w-24 h-24">
611
+ <DraggableDesktopIcon
612
+ id="messages"
613
+ label="Messages"
614
+ iconType="messages"
615
+ initialPosition={{ x: 0, y: 0 }}
616
+ onClick={() => { }}
617
+ onDoubleClick={openMessages}
618
+ />
619
+ </div>
620
+
621
  <div className="pointer-events-auto w-24 h-24">
622
  <DraggableDesktopIcon
623
  id="gemini"
 
661
  />
662
  </div>
663
 
 
 
 
 
664
  <div className="pointer-events-auto w-24 h-24">
665
  <DraggableDesktopIcon
666
  id="flutter-editor"
 
777
  </motion.div>
778
  )}
779
 
780
+ {messagesOpen && (
781
+ <motion.div
782
+ key="messages"
783
+ initial={{ opacity: 0, scale: 0.95 }}
784
+ animate={{
785
+ opacity: messagesMinimized ? 0 : 1,
786
+ scale: messagesMinimized ? 0.9 : 1,
787
+ y: messagesMinimized ? 100 : 0,
788
+ }}
789
+ exit={{ opacity: 0, scale: 0.95 }}
790
+ transition={{ duration: 0.2 }}
791
+ style={{
792
+ pointerEvents: messagesMinimized ? 'none' : 'auto',
793
+ display: messagesMinimized ? 'none' : 'block'
794
+ }}
795
+ >
796
+ <Messages
797
+ onClose={closeMessages}
798
+ onMinimize={() => setMessagesMinimized(true)}
799
+ onFocus={() => bringWindowToFront('messages')}
800
+ zIndex={windowZIndices.messages || 1000}
801
+ />
802
+ </motion.div>
803
+ )}
804
 
805
  {geminiChatOpen && (
806
  <motion.div
 
854
  </motion.div>
855
  )}
856
 
 
 
857
  {flutterCodeEditorOpen && (
858
  <motion.div
859
  key="flutter-code-editor"
 
947
  onAction={handleContextMenuAction}
948
  />
949
 
 
 
950
  {/* Help Modal */}
951
  <HelpModal isOpen={helpModalOpen} onClose={closeHelpModal} />
952
 
app/components/Dock.tsx CHANGED
@@ -5,14 +5,14 @@ import {
5
  Folder,
6
  Calendar as CalendarIcon,
7
  Clock as ClockIcon,
8
-
9
  Sparkle,
10
  Trash,
11
  FolderOpen,
12
  Compass,
13
  Flask,
14
  DeviceMobile,
15
- Function
 
16
  } from '@phosphor-icons/react'
17
  import { motion } from 'framer-motion'
18
  import { DynamicClockIcon } from './DynamicClockIcon'
@@ -29,7 +29,7 @@ interface DockProps {
29
  onOpenFileManager: (path: string) => void
30
  onOpenCalendar: () => void
31
  onOpenClock: () => void
32
-
33
  onOpenGeminiChat: () => void
34
  onCloseAllApps?: () => void
35
  openApps: { [key: string]: boolean }
@@ -81,7 +81,7 @@ export function Dock({
81
  onOpenFileManager,
82
  onOpenCalendar,
83
  onOpenClock,
84
-
85
  onOpenGeminiChat,
86
  onCloseAllApps,
87
  openApps,
@@ -113,6 +113,18 @@ export function Dock({
113
  isActive: openApps['gemini'],
114
  className: ''
115
  },
 
 
 
 
 
 
 
 
 
 
 
 
116
  {
117
  icon: (
118
  <div className="w-full h-full">
 
5
  Folder,
6
  Calendar as CalendarIcon,
7
  Clock as ClockIcon,
 
8
  Sparkle,
9
  Trash,
10
  FolderOpen,
11
  Compass,
12
  Flask,
13
  DeviceMobile,
14
+ Function,
15
+ ChatCircleDots
16
  } from '@phosphor-icons/react'
17
  import { motion } from 'framer-motion'
18
  import { DynamicClockIcon } from './DynamicClockIcon'
 
29
  onOpenFileManager: (path: string) => void
30
  onOpenCalendar: () => void
31
  onOpenClock: () => void
32
+ onOpenMessages: () => void
33
  onOpenGeminiChat: () => void
34
  onCloseAllApps?: () => void
35
  openApps: { [key: string]: boolean }
 
81
  onOpenFileManager,
82
  onOpenCalendar,
83
  onOpenClock,
84
+ onOpenMessages,
85
  onOpenGeminiChat,
86
  onCloseAllApps,
87
  openApps,
 
113
  isActive: openApps['gemini'],
114
  className: ''
115
  },
116
+ {
117
+ icon: (
118
+ <div className="bg-gradient-to-b from-[#4CD964] to-[#2E8B57] w-full h-full rounded-xl flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden">
119
+ <div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-50" />
120
+ <ChatCircleDots size={24} weight="fill" className="text-white md:scale-110 drop-shadow-md" />
121
+ </div>
122
+ ),
123
+ label: 'Messages',
124
+ onClick: onOpenMessages,
125
+ isActive: openApps['messages'],
126
+ className: ''
127
+ },
128
  {
129
  icon: (
130
  <div className="w-full h-full">
app/components/DraggableDesktopIcon.tsx CHANGED
@@ -15,7 +15,8 @@ import {
15
  Code,
16
  Lightning,
17
  Key,
18
- Brain
 
19
  } from '@phosphor-icons/react'
20
  import { DynamicClockIcon } from './DynamicClockIcon'
21
  import { DynamicCalendarIcon } from './DynamicCalendarIcon'
@@ -68,6 +69,14 @@ export function DraggableDesktopIcon({
68
  </div>
69
  )
70
 
 
 
 
 
 
 
 
 
71
  case 'flutter':
72
  return (
73
  <div className="bg-gradient-to-b from-[#54C5F8] to-[#29B6F6] w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden group-hover:shadow-2xl transition-all duration-300">
 
15
  Code,
16
  Lightning,
17
  Key,
18
+ Brain,
19
+ ChatCircleDots
20
  } from '@phosphor-icons/react'
21
  import { DynamicClockIcon } from './DynamicClockIcon'
22
  import { DynamicCalendarIcon } from './DynamicCalendarIcon'
 
69
  </div>
70
  )
71
 
72
+ case 'messages':
73
+ return (
74
+ <div className="bg-gradient-to-b from-[#4CD964] to-[#2E8B57] w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden group-hover:shadow-2xl transition-all duration-300">
75
+ <div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent opacity-50" />
76
+ <ChatCircleDots size={36} weight="fill" className="text-white relative z-10 drop-shadow-md" />
77
+ </div>
78
+ )
79
+
80
  case 'flutter':
81
  return (
82
  <div className="bg-gradient-to-b from-[#54C5F8] to-[#29B6F6] w-full h-full rounded-[22%] flex items-center justify-center shadow-lg border-[0.5px] border-white/20 relative overflow-hidden group-hover:shadow-2xl transition-all duration-300">
app/components/Messages.tsx ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import React, { useState, useEffect, useRef } from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { PaperPlaneRight, MagnifyingGlass, UserCircle, WarningCircle } from '@phosphor-icons/react'
6
+ import Window from './Window'
7
+ import { useKV } from '../hooks/useKV'
8
+
9
+ interface Message {
10
+ id: string
11
+ text: string
12
+ sender: string
13
+ userId: string
14
+ timestamp: number
15
+ }
16
+
17
+ interface MessagesProps {
18
+ onClose: () => void
19
+ onMinimize?: () => void
20
+ onMaximize?: () => void
21
+ onFocus?: () => void
22
+ zIndex?: number
23
+ }
24
+
25
+ export function Messages({ onClose, onMinimize, onMaximize, onFocus, zIndex }: MessagesProps) {
26
+ const [messages, setMessages] = useState<Message[]>([])
27
+ const [inputText, setInputText] = useState('')
28
+ const [isLoading, setIsLoading] = useState(false)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const messagesEndRef = useRef<HTMLDivElement>(null)
31
+
32
+ // Persistent user identity
33
+ const [userId] = useKV<string>('messages-user-id', `user-${Math.random().toString(36).substring(2, 9)}`)
34
+ const [userName, setUserName] = useKV<string>('messages-user-name', 'Guest')
35
+ const [isEditingName, setIsEditingName] = useState(false)
36
+ const [tempName, setTempName] = useState(userName)
37
+
38
+ const fetchMessages = async () => {
39
+ try {
40
+ const res = await fetch('/api/messages')
41
+ if (res.ok) {
42
+ const data = await res.json()
43
+ setMessages(data)
44
+ }
45
+ } catch (err) {
46
+ console.error('Failed to fetch messages', err)
47
+ }
48
+ }
49
+
50
+ // Poll for new messages
51
+ useEffect(() => {
52
+ fetchMessages()
53
+ const interval = setInterval(fetchMessages, 3000)
54
+ return () => clearInterval(interval)
55
+ }, [])
56
+
57
+ // Scroll to bottom on new messages
58
+ useEffect(() => {
59
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
60
+ }, [messages])
61
+
62
+ const handleSend = async (e?: React.FormEvent) => {
63
+ e?.preventDefault()
64
+ if (!inputText.trim() || isLoading) return
65
+
66
+ setIsLoading(true)
67
+ setError(null)
68
+
69
+ try {
70
+ const res = await fetch('/api/messages', {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ text: inputText,
75
+ sender: userName,
76
+ userId: userId
77
+ })
78
+ })
79
+
80
+ const data = await res.json()
81
+
82
+ if (!res.ok) {
83
+ setError(data.error || 'Failed to send message')
84
+ } else {
85
+ setMessages(data)
86
+ setInputText('')
87
+ }
88
+ } catch (err) {
89
+ setError('Network error. Please try again.')
90
+ } finally {
91
+ setIsLoading(false)
92
+ }
93
+ }
94
+
95
+ const handleNameSave = () => {
96
+ if (tempName.trim()) {
97
+ setUserName(tempName.trim())
98
+ setIsEditingName(false)
99
+ }
100
+ }
101
+
102
+ return (
103
+ <Window
104
+ id="messages"
105
+ title="Messages"
106
+ isOpen={true}
107
+ onClose={onClose}
108
+ onMinimize={onMinimize}
109
+ onMaximize={onMaximize}
110
+ onFocus={onFocus}
111
+ zIndex={zIndex}
112
+ width={800}
113
+ height={600}
114
+ x={100}
115
+ y={100}
116
+ className="messages-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
117
+ contentClassName="!bg-transparent"
118
+ headerClassName="!bg-transparent border-b border-white/5"
119
+ >
120
+ <div className="flex h-full text-white overflow-hidden">
121
+ {/* Sidebar */}
122
+ <div className="w-64 border-r border-white/10 bg-black/20 flex flex-col">
123
+ <div className="p-4 border-b border-white/5">
124
+ <div className="relative">
125
+ <MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={14} />
126
+ <input
127
+ type="text"
128
+ placeholder="Search"
129
+ className="w-full bg-white/10 border-none rounded-md py-1.5 pl-9 pr-3 text-xs text-white placeholder-gray-500 focus:ring-1 focus:ring-blue-500 outline-none"
130
+ />
131
+ </div>
132
+ </div>
133
+
134
+ <div className="flex-1 overflow-y-auto p-2 space-y-1">
135
+ <div className="p-3 rounded-lg bg-blue-600/20 border border-blue-500/30 cursor-pointer">
136
+ <div className="flex justify-between items-start">
137
+ <span className="font-semibold text-sm">Global Chat</span>
138
+ <span className="text-[10px] text-gray-400">Now</span>
139
+ </div>
140
+ <div className="text-xs text-gray-400 mt-1 truncate">
141
+ {messages.length > 0 ? messages[messages.length - 1].text : 'No messages yet'}
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ {/* User Profile */}
147
+ <div className="p-4 border-t border-white/10 bg-black/10">
148
+ {isEditingName ? (
149
+ <div className="flex gap-2">
150
+ <input
151
+ type="text"
152
+ value={tempName}
153
+ onChange={(e) => setTempName(e.target.value)}
154
+ className="flex-1 bg-white/10 rounded px-2 py-1 text-sm outline-none border border-blue-500"
155
+ autoFocus
156
+ onKeyDown={(e) => e.key === 'Enter' && handleNameSave()}
157
+ />
158
+ <button onClick={handleNameSave} className="text-xs text-blue-400 font-medium">Save</button>
159
+ </div>
160
+ ) : (
161
+ <div className="flex items-center gap-3 cursor-pointer hover:bg-white/5 p-2 rounded-md transition-colors" onClick={() => {
162
+ setTempName(userName)
163
+ setIsEditingName(true)
164
+ }}>
165
+ <UserCircle size={32} className="text-gray-400" />
166
+ <div className="flex-1 min-w-0">
167
+ <div className="text-sm font-medium truncate">{userName}</div>
168
+ <div className="text-[10px] text-gray-500">Click to change name</div>
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ </div>
174
+
175
+ {/* Chat Area */}
176
+ <div className="flex-1 flex flex-col bg-transparent relative">
177
+ {/* Header */}
178
+ <div className="h-14 border-b border-white/5 flex items-center px-6 bg-[#252525]/30 backdrop-blur-sm justify-between">
179
+ <div className="flex flex-col">
180
+ <span className="text-sm font-semibold">To: Everyone</span>
181
+ <span className="text-[10px] text-gray-400">Global Channel</span>
182
+ </div>
183
+ <div className="flex items-center gap-2 text-orange-400 bg-orange-400/10 px-3 py-1 rounded-full border border-orange-400/20">
184
+ <WarningCircle size={14} />
185
+ <span className="text-[10px] font-medium">Public Chat - Do not share confidential info</span>
186
+ </div>
187
+ </div>
188
+
189
+ {/* Messages List */}
190
+ <div className="flex-1 overflow-y-auto p-6 space-y-1 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2">
191
+ {messages.map((msg, index) => {
192
+ const isMe = msg.userId === userId
193
+ const showHeader = index === 0 || messages[index - 1].userId !== msg.userId
194
+ const showTimestamp = index === 0 || (msg.timestamp - messages[index - 1].timestamp > 300000) // 5 mins
195
+
196
+ return (
197
+ <React.Fragment key={msg.id}>
198
+ {showTimestamp && (
199
+ <div className="text-center my-4">
200
+ <span className="text-[10px] text-gray-500 font-medium">
201
+ {new Date(msg.timestamp).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
202
+ </span>
203
+ </div>
204
+ )}
205
+ <motion.div
206
+ initial={{ opacity: 0, y: 10 }}
207
+ animate={{ opacity: 1, y: 0 }}
208
+ className={`flex flex-col ${isMe ? 'items-end' : 'items-start'} ${showHeader ? 'mt-2' : 'mt-0.5'}`}
209
+ >
210
+ {showHeader && !isMe && (
211
+ <span className="text-[10px] text-gray-500 ml-3 mb-0.5">{msg.sender}</span>
212
+ )}
213
+ <div
214
+ className={`
215
+ max-w-[70%] px-3 py-1.5 text-[13px] leading-relaxed break-words relative group
216
+ ${isMe
217
+ ? 'bg-[#0A84FF] text-white rounded-2xl rounded-br-sm'
218
+ : 'bg-[#3a3a3a] text-gray-100 rounded-2xl rounded-bl-sm'}
219
+ `}
220
+ >
221
+ {msg.text}
222
+ {/* Tail for message bubbles */}
223
+ {isMe && showHeader && (
224
+ <svg className="absolute -bottom-[1px] -right-[6px] w-4 h-4 text-[#0A84FF]" viewBox="0 0 20 20" fill="currentColor">
225
+ <path d="M0 20C5 20 8 15 10 10V20H0Z" />
226
+ </svg>
227
+ )}
228
+ {!isMe && showHeader && (
229
+ <svg className="absolute -bottom-[1px] -left-[6px] w-4 h-4 text-[#3a3a3a] transform scale-x-[-1]" viewBox="0 0 20 20" fill="currentColor">
230
+ <path d="M0 20C5 20 8 15 10 10V20H0Z" />
231
+ </svg>
232
+ )}
233
+ </div>
234
+ {/* Delivered status for me */}
235
+ {isMe && index === messages.length - 1 && (
236
+ <span className="text-[10px] text-gray-500 mr-1 mt-0.5 font-medium">Delivered</span>
237
+ )}
238
+ </motion.div>
239
+ </React.Fragment>
240
+ )
241
+ })}
242
+ <div ref={messagesEndRef} />
243
+ </div>
244
+
245
+ {/* Input Area */}
246
+ <div className="p-4 bg-[#1e1e1e]/50 border-t border-white/10 backdrop-blur-md">
247
+ {error && (
248
+ <motion.div
249
+ initial={{ opacity: 0, y: 10 }}
250
+ animate={{ opacity: 1, y: 0 }}
251
+ exit={{ opacity: 0 }}
252
+ className="flex items-center gap-2 text-red-400 text-xs mb-2 px-2 justify-center"
253
+ >
254
+ <WarningCircle size={14} />
255
+ {error}
256
+ </motion.div>
257
+ )}
258
+ <form onSubmit={handleSend} className="relative flex items-center gap-3 max-w-3xl mx-auto w-full">
259
+ <div className="flex-1 relative">
260
+ <input
261
+ type="text"
262
+ value={inputText}
263
+ onChange={(e) => setInputText(e.target.value)}
264
+ placeholder="iMessage"
265
+ maxLength={200}
266
+ disabled={isLoading}
267
+ className="w-full bg-[#2a2a2a] border border-white/10 rounded-full py-1.5 pl-4 pr-10 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-gray-500 transition-colors"
268
+ />
269
+ <button
270
+ type="submit"
271
+ disabled={!inputText.trim() || isLoading}
272
+ className={`
273
+ absolute right-1 top-1/2 -translate-y-1/2 p-1 rounded-full transition-all
274
+ ${inputText.trim() && !isLoading ? 'bg-[#0A84FF] text-white scale-100' : 'bg-gray-600 text-gray-400 scale-90 opacity-0 pointer-events-none'}
275
+ `}
276
+ >
277
+ <PaperPlaneRight size={14} weight="fill" />
278
+ </button>
279
+ </div>
280
+ </form>
281
+ <div className="text-[9px] text-gray-600 text-center mt-2 font-medium">
282
+ {inputText.length}/200 • Press Enter to send
283
+ </div>
284
+ </div>
285
+ </div> </div>
286
+ </Window>
287
+ )
288
+ }
app/components/Window.tsx CHANGED
@@ -119,7 +119,7 @@ const Window: React.FC<WindowProps> = ({
119
  position: { x: 0, y: 4 }, // 32px for TopBar offset (h-8 = 32px)
120
  size: {
121
  width: typeof window !== 'undefined' ? window.innerWidth : '100vw',
122
- height: typeof window !== 'undefined' ? window.innerHeight - 4 : 'calc(100vh - 4px)'
123
  },
124
  disableDragging: true,
125
  enableResizing: false
 
119
  position: { x: 0, y: 4 }, // 32px for TopBar offset (h-8 = 32px)
120
  size: {
121
  width: typeof window !== 'undefined' ? window.innerWidth : '100vw',
122
+ height: typeof window !== 'undefined' ? window.innerHeight - 120 : 'calc(100vh - 120px)'
123
  },
124
  disableDragging: true,
125
  enableResizing: false
app/data/messages.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "kozrvx3833q",
4
+ "text": "hi babe",
5
+ "sender": "Guest",
6
+ "userId": "user-bl6uf1o",
7
+ "timestamp": 1763788262195
8
+ },
9
+ {
10
+ "id": "7lu3jrs4o5",
11
+ "text": "bye babe",
12
+ "sender": "Guest",
13
+ "userId": "user-bl6uf1o",
14
+ "timestamp": 1763788274778
15
+ },
16
+ {
17
+ "id": "yvi2obxcqh",
18
+ "text": "fuxk",
19
+ "sender": "Guest",
20
+ "userId": "user-bl6uf1o",
21
+ "timestamp": 1763788279726
22
+ },
23
+ {
24
+ "id": "ughhky9l5f",
25
+ "text": "fuck",
26
+ "sender": "Guest",
27
+ "userId": "user-bl6uf1o",
28
+ "timestamp": 1763788283646
29
+ },
30
+ {
31
+ "id": "lydc1y7bg1",
32
+ "text": "dd",
33
+ "sender": "reo",
34
+ "userId": "user-bl6uf1o",
35
+ "timestamp": 1763788313420
36
+ }
37
+ ]