import React, {useCallback, useMemo, useRef} from 'react'; import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View, NativeScrollEvent, NativeSyntheticEvent, } from 'react-native'; import { addDays, eachDayOfInterval, endOfMonth, format, isSameDay, isSameMonth, isWithinInterval, startOfMonth, subDays, addMonths, subMonths } from 'date-fns'; import {useGetEvents} from "@/hooks/firebase/useGetEvents"; import {useAtom, useSetAtom} from "jotai"; import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms"; import {useAuthContext} from "@/contexts/AuthContext"; import * as Device from "expo-device"; 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 CALENDAR_BUFFER_DAYS = 40; export const MonthCalendar: React.FC = () => { const {data: rawEvents, isLoading} = useGetEvents(); const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom); const [mode, setMode] = useAtom(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 dayWidth = (screenWidth - 32) / 7; const isScrolling = useRef(false); const currentScrollX = useRef(screenWidth); const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1; const events = useMemo(() => { if (!rawEvents?.length) return []; if (!selectedDate) return []; const rangeStart = subDays(selectedDate, CALENDAR_BUFFER_DAYS); const rangeEnd = addDays(selectedDate, CALENDAR_BUFFER_DAYS); return rawEvents.filter((event) => { if (!event?.start || !event?.end) { return false; } 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 false; } return startDate <= rangeEnd && endDate >= rangeStart; }); }, [rawEvents, selectedDate]); const onDayPress = useCallback( (date: Date) => { date && setSelectedDate(date); setTimeout(() => { setMode("day"); }, 100) }, [mode, setSelectedNewEndDate, setSelectedDate] ); const getMonthData = useCallback((date: Date) => { const start = startOfMonth(date); const end = endOfMonth(date); const days = eachDayOfInterval({start, end}); // Add padding days at the start 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)); } // Add padding days at the end 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 prevMonth = subMonths(selectedDate, 1); const nextMonth = addMonths(selectedDate, 1); return [ { date: prevMonth, days: getMonthData(prevMonth) }, { date: selectedDate, days: getMonthData(selectedDate) }, { date: nextMonth, days: getMonthData(nextMonth) } ]; }, [selectedDate, getMonthData]); const handleScroll = useCallback((event: NativeSyntheticEvent) => { if (!isScrolling.current) { currentScrollX.current = event.nativeEvent.contentOffset.x; } }, []); const onMomentumScrollEnd = useCallback(() => { if (isScrolling.current) return; const pageWidth = screenWidth; const currentX = currentScrollX.current; if (currentX < pageWidth / 2) { isScrolling.current = true; setSelectedDate(prev => { const newDate = subMonths(prev, 1); // Immediately scroll back to center scrollViewRef.current?.scrollTo({ x: pageWidth, animated: false, }); isScrolling.current = false; currentScrollX.current = pageWidth; return newDate; }); } else if (currentX > pageWidth * 1.5) { isScrolling.current = true; setSelectedDate(prev => { const newDate = addMonths(prev, 1); // Immediately scroll back to center scrollViewRef.current?.scrollTo({ x: pageWidth, animated: false, }); isScrolling.current = false; currentScrollX.current = pageWidth; return newDate; }); } }, [screenWidth, setSelectedDate]); const getEventsForDay = useCallback((date: Date) => { return events?.filter(event => isSameDay(new Date(event.start), date) ) ?? []; }, [events]); const renderEvent = useCallback((event: CalendarEvent) => { return ( onDayPress(event.start)} > {event.title} ); }, [onDayPress]); const renderDay = useCallback((date: Date, index: number) => { const dayEvents = getEventsForDay(date); const isCurrentMonth = isSameMonth(date, selectedDate); const isToday = isSameDay(date, new Date()); const hasMoreEvents = dayEvents.length > MAX_VISIBLE_EVENTS; return ( onDayPress(date)} > {format(date, 'd')} {dayEvents.slice(0, MAX_VISIBLE_EVENTS).map(renderEvent)} {hasMoreEvents && ( {dayEvents.length - MAX_VISIBLE_EVENTS} More )} ); }, [dayWidth, selectedDate, getEventsForDay, onDayPress, renderEvent]); const sortedDaysOfWeek = useMemo(() => { const days = [...DAYS_OF_WEEK]; const sortedDays = days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn)); return sortedDays; }, [weekStartsOn]); return ( {sortedDaysOfWeek.map((day, index) => ( {day} ))} {monthsToRender.map(({date, days}, monthIndex) => ( {days.map((day, index) => renderDay(day, index))} ))} ); }; const HEADER_HEIGHT = 40; const styles = StyleSheet.create({ 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' }, day: { height: '15%', // 100% / 6 weeks padding: 4, borderWidth: 0.5, borderColor: '#eee', }, 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', }, eventsContainer: { flex: 1, marginTop: 2, }, event: { borderRadius: 4, padding: 2, marginVertical: 1, }, eventText: { fontSize: 10, color: '#fff', }, moreEvents: { fontSize: 10, color: '#666', textAlign: 'center', }, });