import { Pressable, Text, View } from "react-native"; import { DAYS, MONTHS, Month, ExpandedEvent, RecurringDeleteMode, } from "@calchat/shared"; import Header from "../../components/Header"; import { EventCard } from "../../components/EventCard"; import { DeleteEventModal } from "../../components/DeleteEventModal"; import { ModalBase } from "../../components/ModalBase"; import { ScrollableDropdown } from "../../components/ScrollableDropdown"; import React, { useCallback, useEffect, useMemo, useState, } from "react"; import { router, useFocusEffect } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { useThemeStore } from "../../stores/ThemeStore"; import BaseBackground from "../../components/BaseBackground"; import { EventService } from "../../services"; import { useEventsStore } from "../../stores"; import { useDropdownPosition } from "../../hooks/useDropdownPosition"; // MonthSelector types and helpers type MonthItem = { id: string; // Format: "YYYY-MM" year: number; monthIndex: number; // 0-11 label: string; // e.g. "January 2024" }; /** * Formats a Date object to a string key in YYYY-MM-DD format. * Used for grouping and looking up events by date. */ const getDateKey = (date: Date): string => { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; }; const generateMonths = ( centerYear: number, centerMonth: number, range: number, ): MonthItem[] => { const months: MonthItem[] = []; for (let offset = -range; offset <= range; offset++) { let year = centerYear; let month = centerMonth + offset; while (month < 0) { month += 12; year--; } while (month > 11) { month -= 12; year++; } months.push({ id: `${year}-${String(month + 1).padStart(2, "0")}`, year, monthIndex: month, label: `${MONTHS[month]} ${year}`, }); } return months; }; const Calendar = () => { const [monthIndex, setMonthIndex] = useState(new Date().getMonth()); const [currentYear, setCurrentYear] = useState(new Date().getFullYear()); const [selectedDate, setSelectedDate] = useState(null); const [overlayVisible, setOverlayVisible] = useState(false); // State for delete modal const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [eventToDelete, setEventToDelete] = useState( null, ); const { events, setEvents, deleteEvent } = useEventsStore(); // Function to load events for current view const loadEvents = useCallback(async () => { try { // Calculate first visible day (up to 6 days before month start) const firstOfMonth = new Date(currentYear, monthIndex, 1); const dayOfWeek = firstOfMonth.getDay(); const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1; const startDate = new Date( currentYear, monthIndex, 1 - daysFromPrevMonth, ); // Calculate last visible day (6 weeks * 7 days = 42 days total) const endDate = new Date(startDate); endDate.setDate(startDate.getDate() + 41); endDate.setHours(23, 59, 59); const loadedEvents = await EventService.getByDateRange( startDate, endDate, ); setEvents(loadedEvents); } catch (error) { console.error("Failed to load events:", error); } }, [monthIndex, currentYear, setEvents]); // Load events when tab gains focus or month/year changes // NOTE: Wrapper needed because loadEvents is async (returns Promise) // and useFocusEffect expects a sync function (optionally returning cleanup) // Also re-open overlay if selectedDate exists (for back navigation from editEvent) useFocusEffect( useCallback(() => { loadEvents(); if (selectedDate) { setOverlayVisible(true); } }, [loadEvents, selectedDate]), ); // Group events by date (YYYY-MM-DD format) // Multi-day events are added to all days they span const eventsByDate = useMemo(() => { const map = new Map(); events.forEach((e) => { const start = new Date(e.occurrenceStart); const end = new Date(e.occurrenceEnd); // Iterate through each day the event spans const current = new Date(start); current.setHours(0, 0, 0, 0); while (current <= end) { const key = getDateKey(current); if (!map.has(key)) map.set(key, []); map.get(key)!.push(e); current.setDate(current.getDate() + 1); } }); return map; }, [events]); const changeMonth = (delta: number) => { setMonthIndex((prev) => { const newIndex = prev + delta; if (newIndex > 11) { setCurrentYear((y) => y + 1); return 0; } if (newIndex < 0) { setCurrentYear((y) => y - 1); return 11; } return newIndex; }); }; const handleDayPress = (date: Date) => { setSelectedDate(date); setOverlayVisible(true); }; const handleCloseOverlay = () => { setSelectedDate(null); setOverlayVisible(false); }; const handleCreateEvent = () => { setOverlayVisible(false); router.push({ pathname: "/editEvent", params: { date: selectedDate?.toISOString() }, }); }; const handleEditEvent = (event?: ExpandedEvent) => { router.push({ pathname: "/editEvent", params: { mode: "calendar", id: event?.id, }, }); }; const handleDeleteEvent = (event: ExpandedEvent) => { // Show delete modal for both recurring and non-recurring events setEventToDelete(event); setDeleteModalVisible(true); }; const handleDeleteConfirm = async (mode: RecurringDeleteMode) => { if (!eventToDelete) return; setDeleteModalVisible(false); const event = eventToDelete; const occurrenceDate = getDateKey(new Date(event.occurrenceStart)); try { if (event.isRecurring) { // Recurring event: use mode and occurrenceDate await EventService.delete(event.id, mode, occurrenceDate); // Reload events to reflect changes await loadEvents(); } else { // Non-recurring event: simple delete await EventService.delete(event.id); deleteEvent(event.id); } } catch (error) { console.error("Failed to delete event:", error); } // Note: Don't clear eventToDelete here - it will be overwritten when opening a new modal. // Clearing it during fade-out animation causes the modal content to flash from recurring to single. }; const handleDeleteCancel = () => { setDeleteModalVisible(false); // Note: Don't clear eventToDelete - keeps modal content stable during fade-out animation }; // Get events for selected date const selectedDateEvents = useMemo(() => { if (!selectedDate) return []; const key = getDateKey(selectedDate); return eventsByDate.get(key) || []; }, [selectedDate, eventsByDate]); return ( ); }; type EventOverlayProps = { visible: boolean; date: Date | null; events: ExpandedEvent[]; onClose: () => void; onEditEvent: (event?: ExpandedEvent) => void; onDeleteEvent: (event: ExpandedEvent) => void; onCreateEvent: () => void; }; const EventOverlay = ({ visible, date, events, onClose, onEditEvent, onDeleteEvent, onCreateEvent, }: EventOverlayProps) => { const { theme } = useThemeStore(); if (!date) return null; const dateString = date.toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "long", year: "numeric", }); const subtitle = `${events.length} ${events.length === 1 ? "Termin" : "Termine"}`; const addEventAttachment = ( Neuen Termin erstellen ); return ( {events.map((event, index) => ( onEditEvent(event)} onDelete={() => onDeleteEvent(event)} /> ))} ); }; type MonthSelectorProps = { modalVisible: boolean; onClose: () => void; position: { top: number; left: number; width: number }; currentYear: number; currentMonthIndex: number; onSelectMonth: (year: number, monthIndex: number) => void; }; const INITIAL_RANGE = 12; // 12 months before and after current const MonthSelector = ({ modalVisible, onClose, position, currentYear, currentMonthIndex, onSelectMonth, }: MonthSelectorProps) => { const [monthSelectorData, setMonthSelectorData] = useState([]); const appendMonths = useCallback( (direction: "start" | "end", count: number) => { setMonthSelectorData((prevData) => { if (prevData.length === 0) return prevData; const newMonths: MonthItem[] = []; const referenceMonth = direction === "start" ? prevData[0] : prevData[prevData.length - 1]; for (let i = 1; i <= count; i++) { const offset = direction === "start" ? -i : i; let year = referenceMonth.year; let month = referenceMonth.monthIndex + offset; while (month < 0) { month += 12; year--; } while (month > 11) { month -= 12; year++; } const newMonth: MonthItem = { id: `${year}-${String(month + 1).padStart(2, "0")}`, year, monthIndex: month, label: `${MONTHS[month]} ${year}`, }; if (direction === "start") { newMonths.unshift(newMonth); } else { newMonths.push(newMonth); } } return direction === "start" ? [...newMonths, ...prevData] : [...prevData, ...newMonths]; }); }, [], ); // Generate fresh data when modal opens, clear when closes useEffect(() => { if (modalVisible) { setMonthSelectorData( generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE), ); } else { setMonthSelectorData([]); } }, [modalVisible, currentYear, currentMonthIndex]); const handleSelect = useCallback( (item: MonthItem) => { onSelectMonth(item.year, item.monthIndex); onClose(); }, [onSelectMonth, onClose], ); return ( item.id} renderItem={(item, theme) => ( {item.label} )} onSelect={handleSelect} height={200} initialScrollIndex={INITIAL_RANGE} onEndReached={() => appendMonths("end", 12)} onStartReached={() => appendMonths("start", 12)} /> ); }; type CalendarHeaderProps = { changeMonth: (delta: number) => void; monthIndex: number; currentYear: number; setMonthIndex: (index: number) => void; setYear: (year: number) => void; }; const CalendarHeader = (props: CalendarHeaderProps) => { const { theme } = useThemeStore(); const dropdown = useDropdownPosition(); const prevMonth = () => props.changeMonth(-1); const nextMonth = () => props.changeMonth(1); return (
{MONTHS[props.monthIndex]} {props.currentYear} { props.setYear(year); props.setMonthIndex(month); }} />
); }; type ChangeMonthButtonProps = { onPress: () => void; icon: "chevron-back" | "chevron-forward"; }; const ChangeMonthButton = (props: ChangeMonthButtonProps) => { const { theme } = useThemeStore(); return ( ); }; const WeekDaysLine = () => { const { theme } = useThemeStore(); return ( {/* TODO: px and gap need fine tuning to perfectly align with the grid */} {DAYS.map((day, i) => ( {day.substring(0, 2).toUpperCase()} ))} ); }; type CalendarGridProps = { month: Month; year: number; eventsByDate: Map; onDayPress: (date: Date) => void; }; const CalendarGrid = (props: CalendarGridProps) => { const { theme } = useThemeStore(); const { baseDate, dateOffset } = useMemo(() => { const monthIndex = MONTHS.indexOf(props.month); const base = new Date(props.year, monthIndex, 1); const offset = base.getDay() === 0 ? 6 : base.getDay() - 1; return { baseDate: base, dateOffset: offset }; }, [props.month, props.year]); // TODO: create array beforehand in a useMemo const createDateFromOffset = (offset: number): Date => { const date = new Date(baseDate); date.setDate(date.getDate() + offset); return date; }; return ( {Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 7 }).map((_, j) => { const date = createDateFromOffset(i * 7 + j - dateOffset); const dateKey = getDateKey(date); const hasEvents = props.eventsByDate.has(dateKey); return ( props.onDayPress(date)} /> ); })} ))} ); }; type SingleDayProps = { date: Date; month: Month; hasEvents: boolean; onPress: () => void; }; const SingleDay = (props: SingleDayProps) => { const { theme } = useThemeStore(); const isSameMonth = MONTHS[props.date.getMonth()] === props.month; return ( {props.date.getDate()} {/* Event indicator dot */} {props.hasEvents && ( )} ); }; export default Calendar;