Reuben_OS / app /components /Calendar.tsx
Reubencf's picture
made some changes with respect to quiz and flutter MCP
f7e5865
'use client'
import React, { useState, useEffect, useMemo, useRef } from 'react'
import {
CaretLeft,
CaretRight,
Plus,
MagnifyingGlass,
CalendarBlank,
List,
Check
} from '@phosphor-icons/react'
import { motion, AnimatePresence } from 'framer-motion'
import { useKV } from '../hooks/useKV'
import Window from './Window'
import { recurringHolidays, getVariableHolidays, Holiday } from '../data/holidays'
interface CalendarProps {
onClose: () => void
onMinimize?: () => void
onMaximize?: () => void
onFocus?: () => void
zIndex?: number
}
interface CalendarEvent extends Holiday {
isCustom?: boolean
}
type ViewMode = 'day' | 'week' | 'month' | 'year'
export function Calendar({ onClose, onMinimize, onMaximize, onFocus, zIndex }: CalendarProps) {
const [windowSize, setWindowSize] = useState({ width: 1100, height: 750 })
const [currentDate, setCurrentDate] = useState(new Date())
const [view, setView] = useState<ViewMode>('month')
const [customEvents, setCustomEvents] = useKV<CalendarEvent[]>('custom-calendar-events', [])
const [searchQuery, setSearchQuery] = useState('')
const [isSearchOpen, setIsSearchOpen] = useState(false)
const scrollContainerRef = useRef<HTMLDivElement>(null)
const [sidebarOpen, setSidebarOpen] = useState(true)
// Handle window resize
useEffect(() => {
const handleResize = () => {
const isMobile = window.innerWidth < 768
setWindowSize({
width: Math.min(1100, window.innerWidth - (isMobile ? 20 : 40)),
height: Math.min(750, window.innerHeight - (isMobile ? 20 : 40))
})
// Auto-close sidebar on mobile
if (isMobile && sidebarOpen) {
setSidebarOpen(false)
}
}
handleResize() // Set initial size
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [sidebarOpen])
// Scroll to 8 AM on mount for day/week views
useEffect(() => {
if ((view === 'day' || view === 'week') && scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 8 * 60
}
}, [view])
// Generate all events
const allEvents = useMemo(() => {
const year = currentDate.getFullYear()
const years = [year - 1, year, year + 1]
let events: CalendarEvent[] = [...customEvents]
years.forEach(y => {
const variable = getVariableHolidays(y)
const recurring = recurringHolidays.map(h => ({
...h,
date: `${y}-${h.date}`
}))
events = [...events, ...variable, ...recurring]
})
return events
}, [currentDate.getFullYear(), customEvents])
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
]
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const hours = Array.from({ length: 24 }, (_, i) => i)
const getDaysInMonth = (year: number, month: number) => {
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
const startingDayOfWeek = firstDay.getDay()
const prevMonthDays = []
const prevMonthLastDay = new Date(year, month, 0).getDate()
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
prevMonthDays.push({
day: prevMonthLastDay - i,
month: month - 1,
year: year,
isCurrentMonth: false
})
}
const currentMonthDays = []
for (let i = 1; i <= daysInMonth; i++) {
currentMonthDays.push({
day: i,
month: month,
year: year,
isCurrentMonth: true
})
}
const nextMonthDays = []
const totalDaysSoFar = prevMonthDays.length + currentMonthDays.length
const remainingCells = 42 - totalDaysSoFar
for (let i = 1; i <= remainingCells; i++) {
nextMonthDays.push({
day: i,
month: month + 1,
year: year,
isCurrentMonth: false
})
}
return [...prevMonthDays, ...currentMonthDays, ...nextMonthDays]
}
const calendarDays = useMemo(() => {
return getDaysInMonth(currentDate.getFullYear(), currentDate.getMonth())
}, [currentDate])
const getEventsForDate = (dateStr: string) => {
return allEvents.filter(e => e.date === dateStr)
}
const isToday = (day: number, month: number, year: number) => {
const today = new Date()
return day === today.getDate() && month === today.getMonth() && year === today.getFullYear()
}
const handlePrev = () => {
const newDate = new Date(currentDate)
if (view === 'month') newDate.setMonth(newDate.getMonth() - 1)
else if (view === 'year') newDate.setFullYear(newDate.getFullYear() - 1)
else if (view === 'week') newDate.setDate(newDate.getDate() - 7)
else if (view === 'day') newDate.setDate(newDate.getDate() - 1)
setCurrentDate(newDate)
}
const handleNext = () => {
const newDate = new Date(currentDate)
if (view === 'month') newDate.setMonth(newDate.getMonth() + 1)
else if (view === 'year') newDate.setFullYear(newDate.getFullYear() + 1)
else if (view === 'week') newDate.setDate(newDate.getDate() + 7)
else if (view === 'day') newDate.setDate(newDate.getDate() + 1)
setCurrentDate(newDate)
}
const handleToday = () => setCurrentDate(new Date())
const getWeekDays = () => {
const startOfWeek = new Date(currentDate)
const day = startOfWeek.getDay()
const diff = startOfWeek.getDate() - day
startOfWeek.setDate(diff)
const days = []
for (let i = 0; i < 7; i++) {
const d = new Date(startOfWeek)
d.setDate(startOfWeek.getDate() + i)
days.push(d)
}
return days
}
const renderHeaderTitle = () => {
if (view === 'year') return <span className="font-semibold text-xl">{currentDate.getFullYear()}</span>
if (view === 'day') {
return (
<div className="flex flex-col leading-tight">
<span className="font-semibold text-xl">{currentDate.getDate()} {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
</div>
)
}
return <span className="font-semibold text-xl">{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}</span>
}
return (
<Window
id="calendar"
title="Calendar"
isOpen={true}
onClose={onClose}
onMinimize={onMinimize}
onMaximize={onMaximize}
onFocus={onFocus}
zIndex={zIndex}
width={windowSize.width}
height={windowSize.height}
x={50}
y={50}
className="calendar-window !bg-[#1e1e1e]/80 !backdrop-blur-2xl border border-white/10 shadow-2xl !rounded-xl overflow-hidden"
contentClassName="!bg-transparent"
headerClassName="!bg-transparent border-b border-white/5"
>
<div className="flex h-full text-white overflow-hidden">
{/* Sidebar */}
<AnimatePresence initial={false}>
{sidebarOpen && (
<motion.div
initial={{ width: 0, opacity: 0 }}
animate={{ width: 240, opacity: 1 }}
exit={{ width: 0, opacity: 0 }}
className="border-r border-white/10 bg-black/20 backdrop-blur-md flex flex-col"
>
<div className="p-4">
<div className="grid grid-cols-7 gap-y-2 text-center mb-4">
{weekDays.map(d => (
<div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
))}
{calendarDays.map((d, i) => {
if (!d.isCurrentMonth) return <div key={i} className="text-gray-600 text-xs">{d.day}</div>
const isCurrentDay = isToday(d.day, d.month, d.year)
const isSelected = d.day === currentDate.getDate() && d.month === currentDate.getMonth()
return (
<div
key={i}
onClick={() => setCurrentDate(new Date(d.year, d.month, d.day))}
className={`
text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer transition-colors
${isCurrentDay ? 'bg-red-500 text-white font-bold' : isSelected ? 'bg-white/20 text-white' : 'text-gray-300 hover:bg-white/10'}
`}
>
{d.day}
</div>
)
})}
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center justify-between text-xs text-gray-400 font-medium px-2">
<span>CALENDARS</span>
</div>
<div className="space-y-1">
{['Home', 'Work', 'Holidays', 'Birthdays'].map((cal, i) => (
<div key={cal} className="flex items-center gap-3 px-2 py-1.5 rounded-md hover:bg-white/5 cursor-pointer group">
<div className={`w-3 h-3 rounded-full border-2 ${i === 0 ? 'border-blue-500 bg-blue-500' :
i === 1 ? 'border-purple-500 bg-purple-500' :
i === 2 ? 'border-green-500 bg-green-500' :
'border-red-500 bg-red-500'
}`} />
<span className="text-sm text-gray-300">{cal}</span>
<Check size={12} className="ml-auto opacity-0 group-hover:opacity-100 text-gray-500" />
</div>
))}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Main Content */}
<div className="flex-1 flex flex-col bg-transparent">
{/* Toolbar */}
<div className="h-12 sm:h-14 flex items-center justify-between px-2 sm:px-6 border-b border-white/10 bg-[#252525]/30 backdrop-blur-sm">
<div className="flex items-center gap-2 sm:gap-4">
<button onClick={() => setSidebarOpen(!sidebarOpen)} className="p-1.5 sm:p-2 hover:bg-white/10 rounded-md transition-colors text-gray-400 hover:text-white">
<List size={18} className="sm:w-5 sm:h-5" />
</button>
<div className="text-base sm:text-2xl font-light tracking-tight hidden sm:block">{renderHeaderTitle()}</div>
<div className="text-sm font-medium tracking-tight sm:hidden">
{view === 'month' && `${monthNames[currentDate.getMonth()].substr(0, 3)} ${currentDate.getFullYear()}`}
{view === 'year' && currentDate.getFullYear()}
{view === 'day' && `${currentDate.getDate()} ${monthNames[currentDate.getMonth()].substr(0, 3)}`}
{view === 'week' && 'Week View'}
</div>
</div>
<div className="flex items-center gap-1 sm:gap-4">
<div className="flex items-center bg-black/20 rounded-lg p-0.5 sm:p-1 border border-white/5">
<button onClick={handlePrev} className="p-1 sm:p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
<CaretLeft size={14} className="sm:w-4 sm:h-4" />
</button>
<button onClick={handleToday} className="hidden sm:block px-3 py-1 text-sm font-medium text-gray-300 hover:text-white transition-colors">
Today
</button>
<button onClick={handleNext} className="p-1 sm:p-1.5 hover:bg-white/10 rounded-md text-gray-400 hover:text-white transition-colors">
<CaretRight size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
<div className="hidden sm:flex items-center bg-black/20 rounded-lg p-1 border border-white/5">
{(['day', 'week', 'month', 'year'] as ViewMode[]).map((v) => (
<button
key={v}
onClick={() => setView(v)}
className={`px-3 py-1 text-xs font-medium rounded-md capitalize transition-all ${view === v
? 'bg-[#444] text-white shadow-sm'
: 'text-gray-400 hover:text-gray-200'
}`}
>
{v}
</button>
))}
</div>
{/* Mobile view selector */}
<select
value={view}
onChange={(e) => setView(e.target.value as ViewMode)}
className="sm:hidden bg-black/20 border border-white/5 rounded-lg px-2 py-1 text-xs text-gray-300 focus:outline-none focus:ring-1 focus:ring-white/20"
>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<button className="hidden sm:block p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
<Plus size={20} />
</button>
<button className="hidden sm:block p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400 hover:text-white">
<MagnifyingGlass size={20} />
</button>
</div>
</div>
{/* View Content */}
<div className="flex-1 overflow-hidden relative">
<AnimatePresence mode="wait">
{view === 'month' && (
<motion.div
key="month"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.02 }}
transition={{ duration: 0.2 }}
className="h-full flex flex-col overflow-auto [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2"
>
<div className="flex-1 flex flex-col">
<div className="grid grid-cols-7 border-b border-white/5 bg-black/10 sticky top-0 z-10 backdrop-blur-sm">
{weekDays.map(day => (
<div key={day} className="py-1 sm:py-2 text-center sm:text-right pr-1 sm:pr-4 text-[10px] sm:text-xs font-medium text-gray-500 uppercase tracking-wider">
<span className="hidden sm:inline">{day}</span>
<span className="sm:hidden">{day.charAt(0)}</span>
</div>
))}
</div>
<div className="flex-1 grid grid-cols-7 grid-rows-6">
{calendarDays.map((dateObj, index) => {
const dateStr = `${dateObj.year}-${String(dateObj.month + 1).padStart(2, '0')}-${String(dateObj.day).padStart(2, '0')}`
const dayEvents = getEventsForDate(dateStr)
const isCurrentDay = isToday(dateObj.day, dateObj.month, dateObj.year)
return (
<div
key={index}
className={`
border-b border-r border-white/5 p-0.5 sm:p-1 relative group transition-colors min-h-[50px] sm:min-h-[80px]
${!dateObj.isCurrentMonth ? 'bg-black/20 text-gray-600' : 'hover:bg-white/5'}
`}
onClick={() => {
setCurrentDate(new Date(dateObj.year, dateObj.month, dateObj.day))
setView('day')
}}
>
<div className="flex justify-center sm:justify-end mb-0.5 sm:mb-1">
<span
className={`
text-xs sm:text-sm font-medium w-5 h-5 sm:w-7 sm:h-7 flex items-center justify-center rounded-full
${isCurrentDay
? 'bg-red-500 text-white shadow-lg shadow-red-900/50'
: dateObj.isCurrentMonth ? 'text-gray-300' : 'text-gray-600'}
`}
>
{dateObj.day}
</span>
</div>
<div className="hidden sm:block space-y-0.5 overflow-y-auto max-h-[calc(100%-1.5rem)] [&::-webkit-scrollbar]:hidden px-0.5">
{dayEvents.slice(0, 3).map((event, i) => (
<div
key={i}
className={`
text-[9px] px-1 py-0.5 rounded-sm truncate cursor-pointer hover:opacity-80
${event.color || 'bg-blue-500'} text-white font-medium shadow-sm backdrop-blur-sm bg-opacity-80
`}
title={event.title}
>
{event.title}
</div>
))}
{dayEvents.length > 3 && (
<div className="text-[8px] text-gray-400 px-1">
+{dayEvents.length - 3} more
</div>
)}
</div>
</div>
)
})}
</div>
</div>
</motion.div>
)}
{view === 'year' && (
<motion.div
key="year"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="h-full overflow-y-auto p-4 md:p-8 [&::-webkit-scrollbar-thumb]:bg-white/20 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-2"
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-8 gap-y-12">
{monthNames.map((month, monthIndex) => {
const days = getDaysInMonth(currentDate.getFullYear(), monthIndex)
return (
<div key={month} className="flex flex-col gap-4">
<h3 className="text-red-500 font-medium text-xl pl-1">{month}</h3>
<div className="grid grid-cols-7 gap-y-3 text-center">
{weekDays.map(d => (
<div key={d} className="text-[10px] text-gray-500 font-medium">{d.charAt(0)}</div>
))}
{days.map((d, i) => {
if (!d.isCurrentMonth) return <div key={i} />
const isCurrentDay = isToday(d.day, d.month, d.year)
return (
<div
key={i}
onClick={() => {
setCurrentDate(new Date(d.year, d.month, d.day))
setView('month')
}}
className={`
text-xs w-6 h-6 flex items-center justify-center rounded-full mx-auto cursor-pointer hover:bg-white/10
${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-gray-300'}
`}
>
{d.day}
</div>
)
})}
</div>
</div>
)
})}
</div>
</motion.div>
)}
{(view === 'week' || view === 'day') && (
<motion.div
key="week-day"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="h-full flex flex-col"
>
<div className="flex border-b border-white/10 shrink-0 pl-14 pr-4 py-2">
{view === 'week' ? getWeekDays().map((d, i) => {
const isCurrentDay = isToday(d.getDate(), d.getMonth(), d.getFullYear())
return (
<div key={i} className="flex-1 text-center border-l border-white/5 first:border-l-0">
<div className={`text-xs font-medium mb-1 uppercase ${isCurrentDay ? 'text-red-500' : 'text-gray-500'}`}>
{weekDays[d.getDay()]}
</div>
<div className={`
text-xl font-light w-8 h-8 flex items-center justify-center rounded-full mx-auto
${isCurrentDay ? 'bg-red-500 text-white font-bold' : 'text-white'}
`}>
{d.getDate()}
</div>
</div>
)
}) : (
<div className="flex-1 px-4">
<div className="text-red-500 font-medium uppercase text-sm">{weekDays[currentDate.getDay()]}</div>
<div className="text-3xl font-light">{currentDate.getDate()}</div>
</div>
)}
</div>
<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">
<div className="absolute top-0 left-0 w-full min-h-full bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjYwIj48bGluZSB4MT0iMCIgeTE9IjYwIiB4Mj0iMTAwJSIgeTI9IjYwIiBzdHJva2U9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiIHN0cm9rZS13aWR0aD0iMSIvPjwvc3ZnPg==')]">
{hours.map((hour) => (
<div key={hour} className="flex h-[60px] group">
<div className="w-14 shrink-0 text-right pr-3 text-xs text-gray-500 -mt-2.5 z-10">
{hour === 0 ? '12 AM' : hour < 12 ? `${hour} AM` : hour === 12 ? '12 PM' : `${hour - 12} PM`}
</div>
<div className="flex-1 border-l border-white/5 relative group-hover:bg-white/[0.02] transition-colors">
{/* Time slots */}
</div>
</div>
))}
{/* Current Time Indicator */}
<div
className="absolute left-14 right-0 border-t border-red-500 z-20 pointer-events-none"
style={{ top: `${(new Date().getHours() * 60 + new Date().getMinutes())}px` }}
>
<div className="absolute -left-1.5 -top-1.5 w-3 h-3 rounded-full bg-red-500" />
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</Window>
)
}