zoomcal-jeffemmett/src/components/calendar/SeasonView.tsx

209 lines
6.4 KiB
TypeScript

'use client'
import { useMemo } from 'react'
import { useCalendarStore } from '@/lib/store'
import {
gregorianToIFC,
getIFCMonthColor,
isLeapYear,
IFC_MONTHS,
IFC_SEASONS,
TemporalGranularity,
} from '@cal/shared'
import { clsx } from 'clsx'
interface MonthGridProps {
year: number
month: number // 1-indexed
calendarType: 'gregorian' | 'ifc'
onDayClick?: (date: Date) => void
}
function MonthGrid({ year, month, calendarType, onDayClick }: MonthGridProps) {
const today = new Date()
today.setHours(0, 0, 0, 0)
const monthData = useMemo(() => {
if (calendarType === 'ifc') {
// IFC: Always 28 days, always starts on Sunday
const days: { day: number; date: Date; isToday: boolean }[] = []
const todayIFC = gregorianToIFC(today)
for (let day = 1; day <= 28; day++) {
// Convert IFC date back to Gregorian for the actual Date object
const ifcDayOfYear = (month - 1) * 28 + day
let gregorianDayOfYear = ifcDayOfYear
if (isLeapYear(year) && ifcDayOfYear >= 169) {
gregorianDayOfYear += 1
}
const date = new Date(year, 0, gregorianDayOfYear)
days.push({
day,
date,
isToday: todayIFC.year === year && todayIFC.month === month && todayIFC.day === day,
})
}
return { days, startDay: 0, daysInMonth: 28 }
} else {
// Gregorian
const firstDay = new Date(year, month - 1, 1)
const lastDay = new Date(year, month, 0)
const daysInMonth = lastDay.getDate()
const startDay = firstDay.getDay()
const days: { day: number; date: Date; isToday: boolean }[] = []
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month - 1, day)
days.push({
day,
date,
isToday: date.getTime() === today.getTime(),
})
}
return { days, startDay, daysInMonth }
}
}, [year, month, calendarType, today])
const monthName = calendarType === 'ifc'
? IFC_MONTHS[month - 1]
: new Date(year, month - 1).toLocaleDateString('en-US', { month: 'long' })
const monthColor = calendarType === 'ifc' ? getIFCMonthColor(month) : undefined
return (
<div className="flex-1 min-w-0">
{/* Month header */}
<div
className="text-center py-2 rounded-t-lg font-semibold"
style={{
backgroundColor: monthColor ? `${monthColor}20` : 'rgb(243 244 246)',
color: monthColor || 'rgb(55 65 81)',
}}
>
{monthName}
</div>
{/* Calendar grid */}
<div className="border border-gray-200 dark:border-gray-700 rounded-b-lg overflow-hidden">
{/* Weekday headers */}
<div className="grid grid-cols-7 bg-gray-50 dark:bg-gray-800">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => (
<div
key={i}
className={clsx(
'text-center text-xs py-1 font-medium',
i === 0 ? 'text-red-500' : i === 6 ? 'text-blue-500' : 'text-gray-500'
)}
>
{day}
</div>
))}
</div>
{/* Day grid */}
<div className="grid grid-cols-7">
{/* Empty cells for start offset */}
{Array.from({ length: monthData.startDay }).map((_, i) => (
<div key={`empty-${i}`} className="aspect-square border-t border-l border-gray-100 dark:border-gray-700" />
))}
{/* Day cells */}
{monthData.days.map(({ day, date, isToday }) => (
<button
key={day}
onClick={() => onDayClick?.(date)}
className={clsx(
'aspect-square flex items-center justify-center text-sm',
'border-t border-l border-gray-100 dark:border-gray-700',
'hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
isToday && 'bg-blue-500 text-white font-bold hover:bg-blue-600'
)}
>
{day}
</button>
))}
</div>
</div>
</div>
)
}
export function SeasonView() {
const {
currentDate,
calendarType,
setCurrentDate,
setTemporalGranularity,
} = useCalendarStore()
const year = currentDate.getFullYear()
// Determine current quarter/season
const currentMonth = calendarType === 'ifc'
? gregorianToIFC(currentDate).month
: currentDate.getMonth() + 1
// Calculate which quarter we're in
const currentQuarter = calendarType === 'ifc'
? IFC_SEASONS.findIndex(s => s.months.includes(currentMonth)) + 1
: Math.ceil(currentMonth / 3)
// Get the months for this quarter
const quarterMonths = useMemo(() => {
if (calendarType === 'ifc') {
const season = IFC_SEASONS[currentQuarter - 1]
return season?.months || [1, 2, 3]
} else {
const startMonth = (currentQuarter - 1) * 3 + 1
return [startMonth, startMonth + 1, startMonth + 2]
}
}, [calendarType, currentQuarter])
const seasonName = calendarType === 'ifc'
? IFC_SEASONS[currentQuarter - 1]?.name || 'Season'
: ['Winter', 'Spring', 'Summer', 'Fall'][currentQuarter - 1] || 'Quarter'
const handleDayClick = (date: Date) => {
setCurrentDate(date)
setTemporalGranularity(TemporalGranularity.DAY)
}
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
{/* Season header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white text-center">
{seasonName} {year}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 text-center mt-1">
Q{currentQuarter} {calendarType === 'ifc' ? 'IFC' : 'Gregorian'} {' '}
{quarterMonths.length} months
</p>
</div>
{/* Three month grid */}
<div className="p-4">
<div className="flex gap-4">
{quarterMonths.map((month) => (
<MonthGrid
key={month}
year={year}
month={month}
calendarType={calendarType}
onDayClick={handleDayClick}
/>
))}
</div>
</div>
{/* Navigation hint */}
<div className="px-4 pb-4">
<p className="text-xs text-center text-gray-400 dark:text-gray-500">
Click any day to zoom in Use to navigate quarters
</p>
</div>
</div>
)
}