mission-control/components/calendar/CalendarGrid.tsx

337 lines
12 KiB
TypeScript

"use client";
import { useState, useMemo } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useCalendar } from "./CalendarContext";
import {
ChevronLeft,
ChevronRight,
Calendar as CalendarIcon,
Clock,
MapPin,
} from "lucide-react";
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addDays,
isSameMonth,
isSameDay,
addMonths,
subMonths,
parseISO,
isToday,
} from "date-fns";
import { cn } from "@/lib/utils";
interface CalendarGridProps {
onEventClick?: (event: CalendarEvent) => void;
}
interface CalendarEvent {
id: string;
summary: string;
description?: string;
start: {
dateTime?: string;
date?: string;
};
end: {
dateTime?: string;
date?: string;
};
location?: string;
calendarId: string;
calendarName: string;
}
function getEventDate(event: { start: { dateTime?: string; date?: string } }): Date {
if (event.start.dateTime) {
return parseISO(event.start.dateTime);
}
if (event.start.date) {
return parseISO(event.start.date);
}
return new Date();
}
function isAllDayEvent(event: { start: { dateTime?: string; date?: string } }): boolean {
return !!event.start.date && !event.start.dateTime;
}
function getCalendarColor(calendarName: string): string {
const colors: Record<string, string> = {
personal: "bg-pink-500 text-white",
work: "bg-blue-500 text-white",
health: "bg-green-500 text-white",
social: "bg-purple-500 text-white",
family: "bg-yellow-500 text-black",
task: "bg-orange-500 text-white",
primary: "bg-blue-600 text-white",
};
const lower = calendarName.toLowerCase();
for (const [key, color] of Object.entries(colors)) {
if (lower.includes(key)) return color;
}
return "bg-muted text-muted-foreground";
}
export function CalendarGrid({ onEventClick }: CalendarGridProps) {
const { events } = useCalendar();
const [currentMonth, setCurrentMonth] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
// Get calendar days
const calendarDays = useMemo(() => {
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(monthStart);
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
const days: Date[] = [];
let day = calendarStart;
while (day <= calendarEnd) {
days.push(day);
day = addDays(day, 1);
}
return days;
}, [currentMonth]);
// Get events for a specific day
const getEventsForDay = (date: Date): CalendarEvent[] => {
return events.filter((event) => {
const eventDate = getEventDate(event);
return isSameDay(eventDate, date);
}).sort((a, b) => {
// All-day events first, then by time
const aAllDay = isAllDayEvent(a);
const bAllDay = isAllDayEvent(b);
if (aAllDay && !bAllDay) return -1;
if (!aAllDay && bAllDay) return 1;
const aDate = getEventDate(a);
const bDate = getEventDate(b);
return aDate.getTime() - bDate.getTime();
});
};
const handlePrevMonth = () => setCurrentMonth(subMonths(currentMonth, 1));
const handleNextMonth = () => setCurrentMonth(addMonths(currentMonth, 1));
const handleToday = () => {
const today = new Date();
setCurrentMonth(today);
setSelectedDate(today);
};
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const selectedDateEvents = selectedDate ? getEventsForDay(selectedDate) : [];
return (
<div className="space-y-4">
{/* Calendar Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold">
{format(currentMonth, "MMMM yyyy")}
</h2>
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon"
onClick={handlePrevMonth}
className="h-8 w-8"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={handleNextMonth}
className="h-8 w-8"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
<Button variant="outline" size="sm" onClick={handleToday}>
Today
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Calendar Grid */}
<Card className="lg:col-span-2">
<CardContent className="p-4">
{/* Week Day Headers */}
<div className="grid grid-cols-7 gap-1 mb-2">
{weekDays.map((day) => (
<div
key={day}
className="text-center text-sm font-medium text-muted-foreground py-2"
>
{day}
</div>
))}
</div>
{/* Calendar Days */}
<div className="grid grid-cols-7 gap-1">
{calendarDays.map((date, index) => {
const dayEvents = getEventsForDay(date);
const isCurrentMonth = isSameMonth(date, currentMonth);
const isTodayDate = isToday(date);
const isSelected = selectedDate && isSameDay(date, selectedDate);
return (
<button
key={index}
onClick={() => setSelectedDate(date)}
className={cn(
"min-h-[80px] sm:min-h-[100px] p-1 sm:p-2 text-left rounded-lg border transition-all",
!isCurrentMonth && "bg-muted/30 text-muted-foreground",
isCurrentMonth && "bg-card hover:bg-accent",
isTodayDate && "border-primary/50 bg-primary/5",
isSelected && "ring-2 ring-primary border-primary"
)}
>
<div className="flex items-center justify-between mb-1">
<span
className={cn(
"text-sm font-medium",
isTodayDate && "text-primary",
!isCurrentMonth && "text-muted-foreground"
)}
>
{format(date, "d")}
</span>
{dayEvents.length > 0 && (
<span className="text-xs text-muted-foreground">
{dayEvents.length}
</span>
)}
</div>
{/* Event Indicators */}
<div className="space-y-1">
{dayEvents.slice(0, 3).map((event, i) => (
<div
key={i}
className={cn(
"text-[10px] px-1.5 py-0.5 rounded truncate",
getCalendarColor(event.calendarName)
)}
title={event.summary}
>
{isAllDayEvent(event) ? (
event.summary
) : (
<span className="flex items-center gap-1">
<span>{format(getEventDate(event), "h:mm a")}</span>
<span className="truncate">{event.summary}</span>
</span>
)}
</div>
))}
{dayEvents.length > 3 && (
<div className="text-[10px] text-muted-foreground px-1.5">
+{dayEvents.length - 3} more
</div>
)}
</div>
</button>
);
})}
</div>
</CardContent>
</Card>
{/* Selected Day Details */}
<Card className="h-fit">
<CardContent className="p-4">
{selectedDate ? (
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg">
{isToday(selectedDate)
? "Today"
: format(selectedDate, "EEEE, MMMM d")}
</h3>
<p className="text-sm text-muted-foreground">
{selectedDateEvents.length} event
{selectedDateEvents.length !== 1 ? "s" : ""}
</p>
</div>
{selectedDateEvents.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CalendarIcon className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">No events scheduled</p>
</div>
) : (
<div className="space-y-3">
{selectedDateEvents.map((event) => (
<div
key={event.id}
onClick={() => onEventClick?.(event)}
className="p-3 rounded-lg border hover:bg-accent cursor-pointer transition-colors"
>
<div className="flex items-start gap-2">
<Badge
className={cn(
"text-[10px] shrink-0",
getCalendarColor(event.calendarName)
)}
>
{event.calendarName}
</Badge>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">
{event.summary}
</p>
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
{isAllDayEvent(event) ? (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
All day
</span>
) : (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{format(getEventDate(event), "h:mm a")}
</span>
)}
</div>
{event.location && (
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
<MapPin className="w-3 h-3" />
<span className="truncate">{event.location}</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
<CalendarIcon className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="text-sm">Select a date to view events</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}