From 580104d052f6000b1b0f9ece20c20b40ef5d5b59 Mon Sep 17 00:00:00 2001 From: Milan Paunovic Date: Thu, 23 Jan 2025 02:16:07 +0100 Subject: [PATCH] Month cal changes --- android/app/src/main/res/values/strings.xml | 2 +- components/pages/calendar/CalendarHeader.tsx | 8 +- .../pages/calendar/CalendarViewSwitch.tsx | 162 ++--- .../pages/calendar/DetailedCalendar.tsx | 86 ++- components/pages/calendar/EventCalendar.tsx | 2 +- components/pages/calendar/MonthCalendar.tsx | 655 ++++++++++++------ package.json | 1 + yarn.lock | 15 +- 8 files changed, 597 insertions(+), 334 deletions(-) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 22880b8..09a92f3 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - "Cally " + \"Cally \" contain false light diff --git a/components/pages/calendar/CalendarHeader.tsx b/components/pages/calendar/CalendarHeader.tsx index 961ee9e..399efa6 100644 --- a/components/pages/calendar/CalendarHeader.tsx +++ b/components/pages/calendar/CalendarHeader.tsx @@ -1,4 +1,4 @@ -import React, {memo} from "react"; +import React, {memo, useState} from "react"; import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib"; import {MaterialIcons} from "@expo/vector-icons"; import {months} from "./constants"; @@ -12,6 +12,7 @@ import {Mode} from "react-native-big-calendar"; export const CalendarHeader = memo(() => { const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const [mode, setMode] = useAtom(modeAtom); + const [tempIndex, setTempIndex] = useState(null); const isTablet = Device.deviceType === Device.DeviceType.TABLET; const segments = isTablet @@ -26,9 +27,12 @@ export const CalendarHeader = memo(() => { selectedMode = ["day", "3days", "month"][index] as Mode; } + setTempIndex(index); + if (selectedMode) { setTimeout(() => { setMode(selectedMode as "day" | "week" | "month" | "3days"); + setTempIndex(null); }, 150); } }; @@ -131,7 +135,7 @@ export const CalendarHeader = memo(() => { outlineWidth={3} segmentLabelStyle={styles.segmentslblStyle} onChangeIndex={handleSegmentChange} - initialIndex={getInitialIndex()} + initialIndex={tempIndex ?? getInitialIndex()} /> diff --git a/components/pages/calendar/CalendarViewSwitch.tsx b/components/pages/calendar/CalendarViewSwitch.tsx index 42cd1d0..b2a642f 100644 --- a/components/pages/calendar/CalendarViewSwitch.tsx +++ b/components/pages/calendar/CalendarViewSwitch.tsx @@ -1,92 +1,94 @@ -import { Text, TouchableOpacity, View } from "react-native-ui-lib"; -import React from "react"; -import { StyleSheet } from "react-native"; -import { useAtom } from "jotai"; -import { isFamilyViewAtom } from "@/components/pages/calendar/atoms"; +import {Text, TouchableOpacity, View} from "react-native-ui-lib"; +import React, {useState, useCallback} from "react"; +import {StyleSheet} from "react-native"; +import {useAtom} from "jotai"; +import {isFamilyViewAtom} from "@/components/pages/calendar/atoms"; const CalendarViewSwitch = () => { - const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom); + const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom); + const [localState, setLocalState] = useState(isFamilyView); - return ( - - { - setIsFamilyView(true); - }} - > - - - Family View - - - + const handleViewChange = useCallback((newValue: boolean) => { + setLocalState(newValue); + setTimeout(() => { + setIsFamilyView(newValue); + }, 150); + }, [setIsFamilyView]); - { - setIsFamilyView(false); - }} - > + return ( - - My View - + handleViewChange(true)} + > + + + Family View + + + + + handleViewChange(false)} + > + + + My View + + + - - - ); + ); }; export default CalendarViewSwitch; const styles = StyleSheet.create({ - switchBtnActive: { - backgroundColor: "#a1a1a1", - borderRadius: 50, - }, - switchBtn: { - backgroundColor: "white", - borderRadius: 50, - }, - switchTxt: { - fontSize: 16, - fontFamily: "Manrope_600SemiBold", - }, -}); + switchBtnActive: { + backgroundColor: "#a1a1a1", + borderRadius: 50, + }, + switchBtn: { + backgroundColor: "white", + borderRadius: 50, + }, + switchTxt: { + fontSize: 16, + fontFamily: "Manrope_600SemiBold", + }, +}); \ No newline at end of file diff --git a/components/pages/calendar/DetailedCalendar.tsx b/components/pages/calendar/DetailedCalendar.tsx index 012ba4a..c2d2a3b 100644 --- a/components/pages/calendar/DetailedCalendar.tsx +++ b/components/pages/calendar/DetailedCalendar.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useRef} from "react"; +import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; import {useAuthContext} from "@/contexts/AuthContext"; import {useAtomValue} from "jotai"; import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms"; @@ -9,8 +9,8 @@ import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls"; import {EventCell} from "@/components/pages/calendar/EventCell"; import {isToday} from "date-fns"; -import { View } from "react-native-ui-lib"; -import { DeviceType } from "expo-device"; +import {View} from "react-native-ui-lib"; +import {DeviceType} from "expo-device"; import * as Device from "expo-device" import {useAtomCallback} from 'jotai/utils' @@ -21,7 +21,35 @@ interface EventCalendarProps { onLoad?: () => void; } -const MemoizedEventCell = React.memo(EventCell); +const HEADER_PROPS = { + dayBarHeight: 60, + headerBottomHeight: 20, +}; + +const BODY_PROPS = { + showNowIndicator: true, + hourFormat: "h:mm a" +}; + +const MODE_TO_DAYS = { + 'week': 7, + '3days': 3, + 'day': 1, + 'month': 1 +}; + +const getContainerProps = (selectedDate: Date) => ({ + hourWidth: 70, + allowPinchToZoom: true, + useHaptic: true, + scrollToNow: true, + initialDate: selectedDate.toISOString(), +}); + +const MemoizedEventCell = React.memo(EventCell, (prev, next) => { + return prev.event.id === next.event.id && + prev.event.lastModified === next.event.lastModified; +}); export const DetailedCalendar: React.FC = React.memo(( { @@ -36,11 +64,16 @@ export const DetailedCalendar: React.FC = React.memo(( const calendarRef = useRef(null); const {data: events} = useGetEvents(); const selectedUser = useAtomValue(selectedUserAtom); + const [customKey, setCustomKey] = useState("defaultKey"); + + const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]); + const containerProps = useMemo(() => getContainerProps(selectedDate), [selectedDate]); const checkModeAndGoToDate = useAtomCallback(useCallback((get) => { const currentMode = get(modeAtom); if ((selectedDate && isToday(selectedDate)) || currentMode === "month") { calendarRef?.current?.goToDate({date: selectedDate}); + setCustomKey(selectedDate.toISOString()) } }, [selectedDate])); @@ -55,35 +88,9 @@ export const DetailedCalendar: React.FC = React.memo(( debouncedOnDateChanged } = useCalendarControls(events ?? []); - const numberOfDays = useMemo(() => { - return mode === 'week' ? 7 : mode === '3days' ? 3 : 1; - }, [mode]); - - const firstDay = useMemo(() => { - return profileData?.firstDayOfWeek === "Mondays" ? 1 : 0; - }, [profileData?.firstDayOfWeek]); - - const headerProps = useMemo(() => ({ - dayBarHeight: 60, - headerBottomHeight: 20, - }), []); - - const bodyProps = useMemo(() => ({ - showNowIndicator: true, - hourFormat: "h:mm a" - }), []); - - const containerProps = useMemo(() => ({ - hourWidth: 70, - allowPinchToZoom: true, - useHaptic: true, - scrollToNow: true, - initialDate: selectedDate.toISOString(), - }), [selectedDate]); - const getAttendees = useCallback((event: any) => { - return familyMembers?.filter(member => event?.attendees?.includes(member?.uid!)) || []; - }, [familyMembers]); + return memoizedFamilyMembers.filter(member => event?.attendees?.includes(member?.uid!)); + }, [memoizedFamilyMembers]); const renderEvent = useCallback((event: any) => { const attendees = getAttendees(event); @@ -94,27 +101,30 @@ export const DetailedCalendar: React.FC = React.memo(( attendees={attendees} /> ); - }, [familyMembers, handlePressEvent, getAttendees]); + }, [getAttendees, handlePressEvent]); return ( - + - {Device.deviceType === DeviceType.TABLET && } + {Device.deviceType === DeviceType.TABLET && ( + + )} ); }); diff --git a/components/pages/calendar/EventCalendar.tsx b/components/pages/calendar/EventCalendar.tsx index 1168302..556e2e0 100644 --- a/components/pages/calendar/EventCalendar.tsx +++ b/components/pages/calendar/EventCalendar.tsx @@ -64,7 +64,7 @@ export const EventCalendar: React.FC = React.memo((props) => return ( - {(isLoading || isSyncing) && ( + {(isLoading || isSyncing) && mode !== 'month' && ( = () => { - 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 DayHeader = React.memo(({day, width}: { day: string; width: string }) => ( + + {day} + +)); - const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1; +const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => ( + + + {event.title} + + +)); - const events = useMemo(() => { - if (!rawEvents?.length) return []; - if (!selectedDate) return []; +interface CalendarEvent { + id: string; + title: string; + start: Date; + end: Date; + color?: string; + weekPosition?: number; +} - const rangeStart = subDays(selectedDate, CALENDAR_BUFFER_DAYS); - const rangeEnd = addDays(selectedDate, CALENDAR_BUFFER_DAYS); +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.color || '#6200ee', + borderRadius: isStart || isEnd ? 4 : 0, + borderTopLeftRadius: isStart ? 4 : 0, + borderBottomLeftRadius: isStart ? 4 : 0, + borderTopRightRadius: isEnd ? 4 : 0, + borderBottomRightRadius: isEnd ? 4 : 0, + padding: 1, + zIndex: 1, + left: isStart ? 2 : 0, + right: isEnd ? 2 : 0, + top: event.weekPosition ? event.weekPosition * 20 : 0, + marginRight: !isEnd ? -1 : 0, + }; - 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)} - > - + return ( + + {isStart && ( + {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; +const Day = React.memo(({ + date, + selectedDate, + events, + multiDayEvents, + dayWidth, + onPress + }: { + date: Date; + selectedDate: Date; + events: CalendarEvent[]; + multiDayEvents: CalendarEvent[]; + dayWidth: number; + onPress: (date: Date) => void; +}) => { + const isCurrentMonth = isSameMonth(date, selectedDate); + const isToday = isSameDay(date, new Date()); - return ( + 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; + + return ( + onDayPress(date)} + style={styles.dayContent} + onPress={() => onPress(date)} > = () => { {format(date, 'd')} + - {dayEvents.slice(0, MAX_VISIBLE_EVENTS).map(renderEvent)} - {hasMoreEvents && ( + {/* Multi-day events first */} + {multiDayEvents.map(event => ( + onPress(event.start)} + /> + ))} + + {/* Single-day events */} + {visibleSingleDayEvents.map(event => ( + onPress(event.start)} + /> + ))} + + {/* Show total number of hidden events */} + {totalHiddenEvents > 0 && ( - {dayEvents.length - MAX_VISIBLE_EVENTS} More + {totalHiddenEvents} More )} - ); - }, [dayWidth, selectedDate, getEventsForDay, onDayPress, renderEvent]); + + ); +}); + + +const findFirstAvailablePosition = (usedPositions: number[]): number => { + let position = 0; + while (usedPositions.includes(position)) { + position++; + } + return position; +}; + +export const MonthCalendar: React.FC = () => { + const {data: rawEvents} = useGetEvents(); + const [selectedDate, setSelectedDate] = useAtom(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 - 32) / 7; + const centerMonth = useRef(selectedDate); + + const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1; + + const events = useMemo(() => { + if (!rawEvents?.length) return new Map(); + if (!selectedDate) return new Map(); + + const eventMap = new Map(); + + 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 dateStr = format(startDate, 'yyyy-MM-dd'); + const existing = eventMap.get(dateStr) || []; + eventMap.set(dateStr, [...existing, event]); + }); + + return eventMap; + }, [rawEvents, selectedDate]); + + 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]); + + const processedEvents = useMemo(() => { + if (!rawEvents?.length || !selectedDate) 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, selectedDate]); + + 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]; - const sortedDays = days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn)); - return sortedDays; + return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn)); }, [weekStartsOn]); + const renderMonth = useCallback(({item}: { item: MonthData }) => ( + + ), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]); + + const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent) => { + const currentMonthIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth); + const currentMonth = monthsToRender[currentMonthIndex]; + + if (currentMonth) { + setSelectedDate(currentMonth.date); + centerMonth.current = currentMonth.date; + } + }, [screenWidth, setSelectedDate, monthsToRender]); + return ( {sortedDaysOfWeek.map((day, index) => ( - - {day} - + ))} - - {monthsToRender.map(({date, days}, monthIndex) => ( - - - {days.map((day, index) => renderDay(day, index))} - - - ))} - + removeClippedSubviews={true} + estimatedItemSize={screenWidth} + estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}} + maintainVisibleContentPosition={{ + minIndexForVisible: 0, + autoscrollToTopThreshold: 10, + }} + /> ); }; +type MonthData = { + date: Date; + days: Date[]; +}; + +const keyExtractor = (item: MonthData, index: number) => `month-${index}`; + +const Month = React.memo(({ + date, + days, + selectedDate, + getEventsForDay, + getMultiDayEventsForDay, + dayWidth, + onPress, + screenWidth + }: { + date: Date; + days: Date[]; + selectedDate: Date; + getEventsForDay: (date: Date) => CalendarEvent[]; + getMultiDayEventsForDay: (date: Date) => CalendarEvent[]; + dayWidth: number; + onPress: (date: Date) => void; + screenWidth: number; +}) => { + 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 ( + + + {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: 0, + left: 0, + right: 0, + zIndex: 1, + }, + dayContent: { + flex: 1, + }, + 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: 4, + borderWidth: 0.5, + borderColor: '#eee', + position: 'relative', + }, container: { flex: 1, height: '100%', @@ -302,12 +554,6 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', justifyContent: 'center' }, - day: { - height: '15%', // 100% / 6 weeks - padding: 4, - borderWidth: 0.5, - borderColor: '#eee', - }, weekDay: { alignItems: 'center', justifyContent: 'center', @@ -342,19 +588,6 @@ const styles = StyleSheet.create({ 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', diff --git a/package.json b/package.json index dbf160e..ab7fdf1 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@react-native/assets-registry": "^0.76.3", "@react-navigation/drawer": "^7.0.0", "@react-navigation/native": "^7.0.0", + "@shopify/flash-list": "^1.7.2", "@tanstack/query-async-storage-persister": "^5.62.7", "@tanstack/react-query": "^5.62.7", "@tanstack/react-query-persist-client": "^5.62.7", diff --git a/yarn.lock b/yarn.lock index 20583f9..22b15a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3226,6 +3226,14 @@ component-type "^1.2.1" join-component "^1.1.0" +"@shopify/flash-list@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.7.2.tgz#a862515a6aae912486d4515909761320d6a6e964" + integrity sha512-rnadpDht/mlJLDM02HBni49EJJQhc51zjJ3diGnTz3MV6U8vK9Hztou+2C5d6bNLb4oZvSG5f7NTWejkipyMLw== + dependencies: + recyclerlistview "4.2.1" + tslib "2.6.3" + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -9974,7 +9982,7 @@ recast@^0.21.0: source-map "~0.6.1" tslib "^2.0.1" -recyclerlistview@^4.0.0: +recyclerlistview@4.2.1, recyclerlistview@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13" integrity sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g== @@ -11143,6 +11151,11 @@ ts-object-utils@0.0.5: resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== +tslib@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"