import React, {useCallback, useMemo, useRef} from 'react'; import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native'; import { addDays, addMonths, eachDayOfInterval, endOfMonth, format, isSameDay, isSameMonth, isWithinInterval, startOfMonth, } from 'date-fns'; import {useGetEvents} from "@/hooks/firebase/useGetEvents"; import {useSetAtom} from "jotai"; import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms"; import {useAuthContext} from "@/contexts/AuthContext"; import {FlashList} from "@shopify/flash-list"; import * as Device from "expo-device"; import {CalendarController} from "@/components/pages/calendar/CalendarController"; interface CalendarEvent { id: string; title: string; start: Date; end: Date; color?: string; } interface CustomMonthCalendarProps { weekStartsOn?: 0 | 1; } const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MAX_VISIBLE_EVENTS = 3; const CENTER_MONTH_INDEX = 12; const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => ( {event.title} )); interface CalendarEvent { id: string; title: string; start: Date; end: Date; color?: string; weekPosition?: number; } const MultiDayEvent = React.memo(({ event, isStart, isEnd, onPress, }: { event: CalendarEvent; dayWidth: number; isStart: boolean; isEnd: boolean; onPress: () => void; }) => { const style = { position: 'absolute' as const, height: 14, backgroundColor: event?.eventColor || '#6200ee', padding: 2, zIndex: 1, left: isStart ? 4 : -0.5, // Extend slightly into the border right: isEnd ? 4 : -0.5, // Extend slightly into the border top: event.weekPosition ? event.weekPosition * 24 : 0, borderRadius: 4, borderTopLeftRadius: isStart ? 4 : 0, borderBottomLeftRadius: isStart ? 4 : 0, borderTopRightRadius: isEnd ? 4 : 0, borderBottomRightRadius: isEnd ? 4 : 0, justifyContent: 'center', }; return ( {isStart && ( {event.title} )} ); }); const Day = React.memo(( { date, events, multiDayEvents, dayWidth, onPress }: { date: Date; events: CalendarEvent[]; multiDayEvents: CalendarEvent[]; dayWidth: number; onPress: (date: Date) => void; }) => { const isCurrentMonth = isSameMonth(date, new Date()); const isToday = isSameDay(date, new Date()); const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length); const singleDayEvents = events.filter(event => !event.isMultiDay); const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots); const totalHiddenEvents = singleDayEvents.length - remainingSlots; const maxMultiDayPosition = multiDayEvents.length > 0 ? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1 : 0; const multiDayEventsHeight = maxMultiDayPosition * 16; // Height for multi-day events return ( onPress(date)} > {format(date, 'd')} {/* Multi-day events container */} {multiDayEvents.map(event => ( onPress(event.start)} /> ))} {/* Single-day events container */} {visibleSingleDayEvents.map(event => ( onPress(event.start)} /> ))} {totalHiddenEvents > 0 && ( {totalHiddenEvents} More )} ); }); export const MonthCalendar: React.FC = () => { const {data: rawEvents} = useGetEvents(); const setSelectedDate = useSetAtom(selectedDateAtom); const setMode = useSetAtom(modeAtom); const {profileData} = useAuthContext(); const scrollViewRef = useRef>(null); const isTablet = Device.deviceType === Device.DeviceType.TABLET; const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width; const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height; const dayWidth = screenWidth / 7; const centerMonth = useRef(new Date()); const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1; const onDayPress = useCallback( (date: Date) => { date && setSelectedDate(date); setTimeout(() => { setMode("day"); }, 100) }, [setSelectedDate, setMode] ); const getMonthData = useCallback((date: Date) => { const start = startOfMonth(date); const end = endOfMonth(date); const days = eachDayOfInterval({start, end}); const firstDay = days[0]; const startPadding = []; let startDay = firstDay.getDay(); while (startDay !== weekStartsOn) { startDay = (startDay - 1 + 7) % 7; startPadding.unshift(addDays(firstDay, -startPadding.length - 1)); } const lastDay = days[days.length - 1]; const endPadding = []; let endDay = lastDay.getDay(); while (endDay !== (weekStartsOn + 6) % 7) { endDay = (endDay + 1) % 7; endPadding.push(addDays(lastDay, endPadding.length + 1)); } return [...startPadding, ...days, ...endPadding]; }, [weekStartsOn]); const monthsToRender = useMemo(() => { const months = []; for (let i = -CENTER_MONTH_INDEX; i <= CENTER_MONTH_INDEX; i++) { const monthDate = addMonths(centerMonth.current, i); months.push({ date: monthDate, days: getMonthData(monthDate) }); } return months; }, [getMonthData, rawEvents]); const processedEvents = useMemo(() => { if (!rawEvents?.length) return { eventMap: new Map(), multiDayEvents: [] }; const eventMap = new Map(); const multiDayEvents: CalendarEvent[] = []; rawEvents.forEach((event) => { if (!event?.start || !event?.end) return; const startDate = event.start instanceof Date ? event.start : new Date(event.start); const endDate = event.end instanceof Date ? event.end : new Date(event.end); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return; const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); if (duration > 1) { multiDayEvents.push({ ...event, isMultiDay: true, start: startDate, end: endDate }); } else { const dateStr = format(startDate, 'yyyy-MM-dd'); const existing = eventMap.get(dateStr) || []; eventMap.set(dateStr, [...existing, {...event, start: startDate, end: endDate}]); } }); multiDayEvents.sort((a, b) => { if (!a.start || !b.start || !a.end || !b.end) return 0; const durationA = a.end.getTime() - a.start.getTime(); const durationB = b.end.getTime() - b.start.getTime(); return durationB - durationA; }); return {eventMap, multiDayEvents}; }, [rawEvents]); const getMultiDayEventsForDay = useCallback((date: Date) => { return processedEvents.multiDayEvents.filter(event => { if (!event.start || !event.end) return false; return isWithinInterval(date, { start: event.start, end: event.end }); }); }, [processedEvents.multiDayEvents]); const getEventsForDay = useCallback((date: Date) => { const dateStr = format(date, 'yyyy-MM-dd'); return processedEvents.eventMap.get(dateStr) || []; }, [processedEvents.eventMap]); const sortedDaysOfWeek = useMemo(() => { const days = [...DAYS_OF_WEEK]; return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn)); }, [weekStartsOn]); const renderMonth = useCallback(({item}: { item: MonthData }) => ( ), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]); return ( ); }; type MonthData = { date: Date; days: Date[]; }; const keyExtractor = (item: MonthData, index: number) => `month-${index}`; const Month = React.memo(({ date, days, getEventsForDay, getMultiDayEventsForDay, dayWidth, onPress, screenWidth, sortedDaysOfWeek }: { date: Date; days: Date[]; getEventsForDay: (date: Date) => CalendarEvent[]; getMultiDayEventsForDay: (date: Date) => CalendarEvent[]; dayWidth: number; onPress: (date: Date) => void; screenWidth: number; sortedDaysOfWeek: string[]; }) => { const weeks = useMemo(() => { const result = []; for (let i = 0; i < days.length; i += 7) { result.push(days.slice(i, i + 7)); } return result; }, [days]); const eventPositions = useMemo(() => { const positions = new Map(); const weekTracking = new Map>(); weeks.forEach((week, weekIndex) => { const activeEvents = new Set(); week.forEach(day => { const events = getMultiDayEventsForDay(day); events.forEach(event => { activeEvents.add(event.id); }); }); weekTracking.set(weekIndex, activeEvents); activeEvents.forEach(eventId => { if (!positions.has(eventId)) { const prevWeekEvents = weekIndex > 0 ? weekTracking.get(weekIndex - 1) : new Set(); const usedPositions = new Set(); if (prevWeekEvents) { prevWeekEvents.forEach(prevEventId => { if (activeEvents.has(prevEventId)) { usedPositions.add(positions.get(prevEventId) || 0); } }); } let position = 0; while (usedPositions.has(position)) { position++; } positions.set(eventId, position); } }); }); return positions; }, [weeks, getMultiDayEventsForDay]); return ( {format(date, 'MMMM yyyy')} {sortedDaysOfWeek.map((day, index) => ( {day} ))} {weeks.map((week, weekIndex) => ( {week.map((date, dayIndex) => { const multiDayEvents = getMultiDayEventsForDay(date).map(event => ({ ...event, weekPosition: eventPositions.get(event.id) || 0 })); return ( ); })} ))} ); }); const HEADER_HEIGHT = 40; const styles = StyleSheet.create({ multiDayContainer: { position: 'absolute', top: 29, left: 0, right: 0, bottom: 0, zIndex: 1, }, dayContent: { flex: 1, padding: 4, // Move padding here instead }, eventsContainer: { flex: 1, marginTop: 2, position: 'relative', }, event: { borderRadius: 4, padding: 1, marginVertical: 1, height: 14, }, eventText: { fontSize: 10, color: '#fff', fontWeight: '500', }, day: { height: '14%', padding: 0, borderWidth: 0.5, borderColor: '#eee', position: 'relative', overflow: 'visible', }, container: { flex: 1, height: '100%', }, header: { flexDirection: 'row', justifyContent: 'center', paddingHorizontal: 16, paddingVertical: 8, }, scrollView: { flex: 1, }, monthContainer: { flex: 1, }, daysGrid: { flex: 1, flexDirection: 'row', flexWrap: 'wrap', // justifyContent: 'center' }, weekDay: { alignItems: 'center', justifyContent: 'center', height: HEADER_HEIGHT, }, scrollContent: { flex: 1, }, weekDayText: { fontSize: 12, fontWeight: '600', color: '#666', }, dateContainer: { minWidth: 20, height: 24, alignItems: 'center', justifyContent: 'center', borderRadius: 12, }, todayContainer: { backgroundColor: '#6200ee', }, dateText: { fontSize: 12, fontWeight: '500', color: '#333', }, todayText: { color: '#fff', }, outsideMonthText: { color: '#ccc', }, moreEvents: { fontSize: 10, color: '#666', textAlign: 'center', }, monthHeader: { paddingVertical: 12, }, monthText: { fontSize: 16, fontWeight: '600', color: '#333', textAlign: 'center', marginBottom: 8, }, weekDayRow: { flexDirection: 'row', justifyContent: 'space-around', paddingHorizontal: 0, }, }); export default MonthCalendar;