'use client' import { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { Button } from '@/components/ui/button' import { BookmarkCard } from '@/components/bookmark-card' import { BookmarkDetailSheet } from '@/components/bookmark-detail-sheet' import { ChevronLeft, ChevronRight, Share2 } from 'lucide-react' import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isSameMonth, addMonths, subMonths, getDay } from 'date-fns' import type { Bookmark } from '@/components/app-shell' import { Input } from '@/components/ui/input' import { toast } from 'sonner' import { fetchBookmarks } from '@/lib/bookmarks' // Review L-4: a fixed, obviously-not-current placeholder. The real month and // today are set after mount (to avoid an SSR/client hydration mismatch), and // the calendar renders a skeleton until then instead of briefly presenting // this epoch as the current month. const INITIAL_DATE = new Date(2000, 0, 1) export function CalendarClient() { const [currentMonth, setCurrentMonth] = useState(INITIAL_DATE) const [today, setToday] = useState(INITIAL_DATE) const [ready, setReady] = useState(false) const [bookmarks, setBookmarks] = useState([]) const [selectedDay, setSelectedDay] = useState(null) const [selectedBookmark, setSelectedBookmark] = useState(null) const [loading, setLoading] = useState(true) const [sharedDates, setSharedDates] = useState([]) const [shareQuery, setShareQuery] = useState('') const [shareUsers, setShareUsers] = useState([]) // Debounced user search; failures keep the previous results silently. useEffect(() => { const q = shareQuery.trim() const t = setTimeout(async () => { if (q.length < 2) { setShareUsers([]); return } try { const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`) if (res.ok) setShareUsers(await res.json()) } catch { /* transient; retried on next keystroke */ } }, q.length < 2 ? 0 : 250) return () => clearTimeout(t) }, [shareQuery]) useEffect(() => { const timer = setTimeout(() => { const now = new Date(); setCurrentMonth(now); setToday(now); setReady(true) }, 0) return () => clearTimeout(timer) }, []) // Review M-11: bookmark-change events and slow initial loads can overlap; // only the newest pass may publish its data. const loadSeq = useRef(0) const load = useCallback(async () => { const seq = ++loadSeq.current setLoading(true) try { const [mine, shared, dates] = await Promise.all([fetchBookmarks(), fetchBookmarks(new URLSearchParams({ shared: 'true' })), fetch('/api/shared-dates')]) if (seq !== loadSeq.current) return const sharedDatesJson = dates.ok ? await dates.json() : null if (seq !== loadSeq.current) return setBookmarks([...mine, ...shared]) if (sharedDatesJson) setSharedDates(sharedDatesJson) } catch { if (seq === loadSeq.current) toast.error('Could not load calendar') } finally { if (seq === loadSeq.current) setLoading(false) } }, []) useEffect(() => { const timer = setTimeout(load, 0); window.addEventListener('computer:bookmarks-changed', load); return () => { clearTimeout(timer); window.removeEventListener('computer:bookmarks-changed', load) } }, [load]) const days = useMemo(() => { const start = startOfMonth(currentMonth) const end = endOfMonth(currentMonth) return eachDayOfInterval({ start, end }) }, [currentMonth]) const startDayOfWeek = getDay(startOfMonth(currentMonth)) const bookmarksByDay = useMemo(() => { const map: Record = {} for (const b of bookmarks) { if (b?.dueDate) { const key = b.dueDate.slice(0, 10) if (!map[key]) map[key] = [] map[key].push(b) } } return map }, [bookmarks]) const selectedBookmarks = useMemo(() => { if (!selectedDay) return [] const key = format(selectedDay, 'yyyy-MM-dd') return bookmarksByDay[key] ?? [] }, [selectedDay, bookmarksByDay]) if (!ready) { return (

Calendar

Bookmarks organized by due date

) } return (

Calendar

Bookmarks organized by due date

{format(currentMonth, 'MMMM yyyy')}
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => (
{d}
))} {Array.from({ length: startDayOfWeek }).map((_, i) => (
))} {days.map(day => { const key = format(day, 'yyyy-MM-dd') const dayBookmarks = bookmarksByDay[key] ?? [] const isSelected = selectedDay ? isSameDay(day, selectedDay) : false const isToday = isSameDay(day, today) return (
setSelectedDay(day)} onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedDay(day) } }} className={`bg-card min-h-[80px] p-1.5 cursor-pointer transition-colors hover:bg-muted/50 focus-visible:outline-2 focus-visible:outline-primary ${ isSelected ? 'ring-2 ring-primary ring-inset' : '' }`} > {format(day, 'd')}
{sharedDates.filter(s => s.date.slice(0, 10) === key).map(s =>

Shared: {s.creator.name || s.creator.email}

)} {dayBookmarks.slice(0, 2).map((b: Bookmark) => (
{ e.stopPropagation(); setSelectedBookmark(b) }} onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); setSelectedBookmark(b) } }}> {b.title ?? 'Untitled'}
))} {dayBookmarks.length > 2 && (
+{dayBookmarks.length - 2} more
)}
) })}
{selectedDay && (

{format(selectedDay, 'EEEE, MMMM d, yyyy')}

setShareQuery(e.target.value)} />
{/* Review L-9: sharing a date is broader than the old label said. */}

Recipients will see your bookmarks due that day, including their notes.

{shareUsers.map(u => )} {sharedDates.filter(s => s.date.slice(0, 10) === format(selectedDay, 'yyyy-MM-dd')).map(s =>
{s.creator.name || s.creator.email} shared with {s.recipient.name || s.recipient.email}
)} {selectedBookmarks.length === 0 ? (

No bookmarks on this date

) : (
{selectedBookmarks.map((b: Bookmark) => ( setSelectedBookmark(b)} /> ))}
)}
)} setSelectedBookmark(null)} onUpdate={load} />
) }