mirror of
https://github.com/urosran/cally.git
synced 2025-07-10 15:17:17 +00:00
Month cal changes
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">"Cally "</string>
|
<string name="app_name">\"Cally \"</string>
|
||||||
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
|
||||||
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
|
||||||
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>
|
||||||
|
@ -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 {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
|
||||||
import {MaterialIcons} from "@expo/vector-icons";
|
import {MaterialIcons} from "@expo/vector-icons";
|
||||||
import {months} from "./constants";
|
import {months} from "./constants";
|
||||||
@ -12,6 +12,7 @@ import {Mode} from "react-native-big-calendar";
|
|||||||
export const CalendarHeader = memo(() => {
|
export const CalendarHeader = memo(() => {
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
const [mode, setMode] = useAtom(modeAtom);
|
const [mode, setMode] = useAtom(modeAtom);
|
||||||
|
const [tempIndex, setTempIndex] = useState<number | null>(null);
|
||||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||||
|
|
||||||
const segments = isTablet
|
const segments = isTablet
|
||||||
@ -26,9 +27,12 @@ export const CalendarHeader = memo(() => {
|
|||||||
selectedMode = ["day", "3days", "month"][index] as Mode;
|
selectedMode = ["day", "3days", "month"][index] as Mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTempIndex(index);
|
||||||
|
|
||||||
if (selectedMode) {
|
if (selectedMode) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setMode(selectedMode as "day" | "week" | "month" | "3days");
|
setMode(selectedMode as "day" | "week" | "month" | "3days");
|
||||||
|
setTempIndex(null);
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -131,7 +135,7 @@ export const CalendarHeader = memo(() => {
|
|||||||
outlineWidth={3}
|
outlineWidth={3}
|
||||||
segmentLabelStyle={styles.segmentslblStyle}
|
segmentLabelStyle={styles.segmentslblStyle}
|
||||||
onChangeIndex={handleSegmentChange}
|
onChangeIndex={handleSegmentChange}
|
||||||
initialIndex={getInitialIndex()}
|
initialIndex={tempIndex ?? getInitialIndex()}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
||||||
import React from "react";
|
import React, {useState, useCallback} from "react";
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from "react-native";
|
||||||
import {useAtom} from "jotai";
|
import {useAtom} from "jotai";
|
||||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||||
|
|
||||||
const CalendarViewSwitch = () => {
|
const CalendarViewSwitch = () => {
|
||||||
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
|
const [localState, setLocalState] = useState(isFamilyView);
|
||||||
|
|
||||||
|
const handleViewChange = useCallback((newValue: boolean) => {
|
||||||
|
setLocalState(newValue);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsFamilyView(newValue);
|
||||||
|
}, 150);
|
||||||
|
}, [setIsFamilyView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -19,30 +27,26 @@ const CalendarViewSwitch = () => {
|
|||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
// iOS shadow
|
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {width: 0, height: 2},
|
shadowOffset: {width: 0, height: 2},
|
||||||
shadowOpacity: 0.25,
|
shadowOpacity: 0.25,
|
||||||
shadowRadius: 3.84,
|
shadowRadius: 3.84,
|
||||||
// Android shadow (elevation)
|
|
||||||
elevation: 6,
|
elevation: 6,
|
||||||
}}
|
}}
|
||||||
centerV
|
centerV
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => handleViewChange(true)}
|
||||||
setIsFamilyView(true);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
centerV
|
centerV
|
||||||
centerH
|
centerH
|
||||||
height={40}
|
height={40}
|
||||||
paddingH-15
|
paddingH-15
|
||||||
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
style={localState ? styles.switchBtnActive : styles.switchBtn}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={isFamilyView ? "white" : "#a1a1a1"}
|
color={localState ? "white" : "#a1a1a1"}
|
||||||
style={styles.switchTxt}
|
style={styles.switchTxt}
|
||||||
>
|
>
|
||||||
Family View
|
Family View
|
||||||
@ -51,19 +55,17 @@ const CalendarViewSwitch = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => handleViewChange(false)}
|
||||||
setIsFamilyView(false);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
centerV
|
centerV
|
||||||
centerH
|
centerH
|
||||||
height={40}
|
height={40}
|
||||||
paddingH-15
|
paddingH-15
|
||||||
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
style={!localState ? styles.switchBtnActive : styles.switchBtn}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
color={!isFamilyView ? "white" : "#a1a1a1"}
|
color={!localState ? "white" : "#a1a1a1"}
|
||||||
style={styles.switchTxt}
|
style={styles.switchTxt}
|
||||||
>
|
>
|
||||||
My View
|
My View
|
||||||
|
@ -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 {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useAtomValue} from "jotai";
|
import {useAtomValue} from "jotai";
|
||||||
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms";
|
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||||
@ -21,7 +21,35 @@ interface EventCalendarProps {
|
|||||||
onLoad?: () => void;
|
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<EventCalendarProps> = React.memo((
|
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
||||||
{
|
{
|
||||||
@ -36,11 +64,16 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
|||||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||||
const {data: events} = useGetEvents();
|
const {data: events} = useGetEvents();
|
||||||
const selectedUser = useAtomValue(selectedUserAtom);
|
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 checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
|
||||||
const currentMode = get(modeAtom);
|
const currentMode = get(modeAtom);
|
||||||
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
||||||
calendarRef?.current?.goToDate({date: selectedDate});
|
calendarRef?.current?.goToDate({date: selectedDate});
|
||||||
|
setCustomKey(selectedDate.toISOString())
|
||||||
}
|
}
|
||||||
}, [selectedDate]));
|
}, [selectedDate]));
|
||||||
|
|
||||||
@ -55,35 +88,9 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
|||||||
debouncedOnDateChanged
|
debouncedOnDateChanged
|
||||||
} = useCalendarControls(events ?? []);
|
} = 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) => {
|
const getAttendees = useCallback((event: any) => {
|
||||||
return familyMembers?.filter(member => event?.attendees?.includes(member?.uid!)) || [];
|
return memoizedFamilyMembers.filter(member => event?.attendees?.includes(member?.uid!));
|
||||||
}, [familyMembers]);
|
}, [memoizedFamilyMembers]);
|
||||||
|
|
||||||
const renderEvent = useCallback((event: any) => {
|
const renderEvent = useCallback((event: any) => {
|
||||||
const attendees = getAttendees(event);
|
const attendees = getAttendees(event);
|
||||||
@ -94,27 +101,30 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
|||||||
attendees={attendees}
|
attendees={attendees}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [familyMembers, handlePressEvent, getAttendees]);
|
}, [getAttendees, handlePressEvent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CalendarContainer
|
<CalendarContainer
|
||||||
ref={calendarRef}
|
ref={calendarRef}
|
||||||
{...containerProps}
|
{...containerProps}
|
||||||
numberOfDays={numberOfDays}
|
numberOfDays={MODE_TO_DAYS[mode]}
|
||||||
calendarWidth={calendarWidth}
|
calendarWidth={calendarWidth}
|
||||||
onDateChanged={debouncedOnDateChanged}
|
onDateChanged={debouncedOnDateChanged}
|
||||||
firstDay={firstDay}
|
firstDay={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
|
||||||
events={formattedEvents ?? []}
|
events={formattedEvents ?? []}
|
||||||
onPressEvent={handlePressEvent}
|
onPressEvent={handlePressEvent}
|
||||||
onPressBackground={handlePressCell}
|
onPressBackground={handlePressCell}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
|
key={customKey}
|
||||||
>
|
>
|
||||||
<CalendarHeader {...headerProps} />
|
<CalendarHeader {...HEADER_PROPS} />
|
||||||
<CalendarBody
|
<CalendarBody
|
||||||
{...bodyProps}
|
{...BODY_PROPS}
|
||||||
renderEvent={renderEvent}
|
renderEvent={renderEvent}
|
||||||
/>
|
/>
|
||||||
{Device.deviceType === DeviceType.TABLET && <View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>}
|
{Device.deviceType === DeviceType.TABLET && (
|
||||||
|
<View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>
|
||||||
|
)}
|
||||||
</CalendarContainer>
|
</CalendarContainer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -64,7 +64,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
{(isLoading || isSyncing) && (
|
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
exiting={FadeOut.duration(300)}
|
exiting={FadeOut.duration(300)}
|
||||||
style={styles.loadingContainer}
|
style={styles.loadingContainer}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, {useCallback, useMemo, useRef} from 'react';
|
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
@ -16,16 +15,14 @@ import {
|
|||||||
format,
|
format,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
isSameMonth,
|
isSameMonth,
|
||||||
isWithinInterval,
|
|
||||||
startOfMonth,
|
startOfMonth,
|
||||||
subDays,
|
addMonths, startOfWeek, isWithinInterval,
|
||||||
addMonths,
|
|
||||||
subMonths
|
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||||
import {useAtom, useSetAtom} from "jotai";
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {FlashList} from "@shopify/flash-list";
|
||||||
import * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
|
|
||||||
interface CalendarEvent {
|
interface CalendarEvent {
|
||||||
@ -42,175 +39,103 @@ interface CustomMonthCalendarProps {
|
|||||||
|
|
||||||
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
const MAX_VISIBLE_EVENTS = 3 ;
|
const MAX_VISIBLE_EVENTS = 3 ;
|
||||||
const CALENDAR_BUFFER_DAYS = 40;
|
const CENTER_MONTH_INDEX = 24;
|
||||||
|
|
||||||
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|
||||||
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<ScrollView>(null);
|
const DayHeader = React.memo(({day, width}: { day: string; width: string }) => (
|
||||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
<View style={[styles.weekDay, {width}]}>
|
||||||
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
|
<Text style={styles.weekDayText}>{day}</Text>
|
||||||
const dayWidth = (screenWidth - 32) / 7;
|
</View>
|
||||||
const isScrolling = useRef(false);
|
));
|
||||||
const currentScrollX = useRef(screenWidth);
|
|
||||||
|
|
||||||
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
|
||||||
|
|
||||||
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<NativeScrollEvent>) => {
|
|
||||||
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 (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={event.id}
|
|
||||||
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]}
|
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]}
|
||||||
onPress={() => onDayPress(event.start)}
|
onPress={onPress}
|
||||||
>
|
>
|
||||||
<Text style={styles.eventText} numberOfLines={1}>
|
<Text style={styles.eventText} numberOfLines={1}>
|
||||||
{event.title}
|
{event.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
));
|
||||||
}, [onDayPress]);
|
|
||||||
|
|
||||||
const renderDay = useCallback((date: Date, index: number) => {
|
interface CalendarEvent {
|
||||||
const dayEvents = getEventsForDay(date);
|
id: string;
|
||||||
const isCurrentMonth = isSameMonth(date, selectedDate);
|
title: string;
|
||||||
const isToday = isSameDay(date, new Date());
|
start: Date;
|
||||||
const hasMoreEvents = dayEvents.length > MAX_VISIBLE_EVENTS;
|
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.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 (
|
return (
|
||||||
|
<TouchableOpacity style={style} onPress={onPress}>
|
||||||
|
{isStart && (
|
||||||
|
<Text style={[styles.eventText]} numberOfLines={1}>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={[styles.day, { width: dayWidth }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={index}
|
style={styles.dayContent}
|
||||||
style={[styles.day, {width: dayWidth}]}
|
onPress={() => onPress(date)}
|
||||||
onPress={() => onDayPress(date)}
|
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.dateContainer,
|
styles.dateContainer,
|
||||||
@ -224,62 +149,389 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
|||||||
{format(date, 'd')}
|
{format(date, 'd')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.eventsContainer}>
|
<View style={styles.eventsContainer}>
|
||||||
{dayEvents.slice(0, MAX_VISIBLE_EVENTS).map(renderEvent)}
|
{/* Multi-day events first */}
|
||||||
{hasMoreEvents && (
|
{multiDayEvents.map(event => (
|
||||||
|
<MultiDayEvent
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
dayWidth={dayWidth}
|
||||||
|
isStart={isSameDay(date, event.start)}
|
||||||
|
isEnd={isSameDay(date, event.end)}
|
||||||
|
onPress={() => onPress(event.start)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Single-day events */}
|
||||||
|
{visibleSingleDayEvents.map(event => (
|
||||||
|
<Event
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
onPress={() => onPress(event.start)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Show total number of hidden events */}
|
||||||
|
{totalHiddenEvents > 0 && (
|
||||||
<Text style={styles.moreEvents}>
|
<Text style={styles.moreEvents}>
|
||||||
{dayEvents.length - MAX_VISIBLE_EVENTS} More
|
{totalHiddenEvents} More
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}, [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<CustomMonthCalendarProps> = () => {
|
||||||
|
const {data: rawEvents} = useGetEvents();
|
||||||
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
|
const setMode = useSetAtom(modeAtom);
|
||||||
|
const {profileData} = useAuthContext();
|
||||||
|
|
||||||
|
const scrollViewRef = useRef<FlashList<any>>(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 sortedDaysOfWeek = useMemo(() => {
|
||||||
const days = [...DAYS_OF_WEEK];
|
const days = [...DAYS_OF_WEEK];
|
||||||
const sortedDays = days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn));
|
return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn));
|
||||||
return sortedDays;
|
|
||||||
}, [weekStartsOn]);
|
}, [weekStartsOn]);
|
||||||
|
|
||||||
|
const renderMonth = useCallback(({item}: { item: MonthData }) => (
|
||||||
|
<Month
|
||||||
|
date={item.date}
|
||||||
|
days={item.days}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
getEventsForDay={getEventsForDay}
|
||||||
|
getMultiDayEventsForDay={getMultiDayEventsForDay}
|
||||||
|
dayWidth={dayWidth}
|
||||||
|
onPress={onDayPress}
|
||||||
|
screenWidth={screenWidth}
|
||||||
|
/>
|
||||||
|
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
||||||
|
|
||||||
|
const onMomentumScrollEnd = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
{sortedDaysOfWeek.map((day, index) => (
|
{sortedDaysOfWeek.map((day, index) => (
|
||||||
<View key={index} style={[styles.weekDay, {width: `${100 / 7}%`}]}>
|
<DayHeader key={index} day={day} width={`${100 / 7}%`}/>
|
||||||
<Text style={styles.weekDayText}>{day}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<ScrollView
|
<FlashList
|
||||||
ref={scrollViewRef}
|
ref={scrollViewRef}
|
||||||
|
data={monthsToRender}
|
||||||
|
renderItem={renderMonth}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
horizontal
|
horizontal
|
||||||
pagingEnabled
|
pagingEnabled
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
onScroll={handleScroll}
|
initialScrollIndex={CENTER_MONTH_INDEX}
|
||||||
onMomentumScrollEnd={onMomentumScrollEnd}
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
||||||
scrollEventThrottle={16}
|
removeClippedSubviews={true}
|
||||||
contentOffset={{x: screenWidth, y: 0}}
|
estimatedItemSize={screenWidth}
|
||||||
style={{flex: 1}}
|
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
||||||
>
|
maintainVisibleContentPosition={{
|
||||||
{monthsToRender.map(({date, days}, monthIndex) => (
|
minIndexForVisible: 0,
|
||||||
<View
|
autoscrollToTopThreshold: 10,
|
||||||
key={monthIndex}
|
}}
|
||||||
style={[styles.scrollView, {width: screenWidth}]}
|
/>
|
||||||
>
|
|
||||||
<View style={styles.daysGrid}>
|
|
||||||
{days.map((day, index) => renderDay(day, index))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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<string, number>();
|
||||||
|
const weekTracking = new Map<number, Set<string>>();
|
||||||
|
|
||||||
|
weeks.forEach((week, weekIndex) => {
|
||||||
|
const activeEvents = new Set<string>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
const usedPositions = new Set<number>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={[styles.scrollView, {width: screenWidth}]}>
|
||||||
|
<View style={styles.daysGrid}>
|
||||||
|
{weeks.map((week, weekIndex) => (
|
||||||
|
<React.Fragment key={weekIndex}>
|
||||||
|
{week.map((date, dayIndex) => {
|
||||||
|
const multiDayEvents = getMultiDayEventsForDay(date).map(event => ({
|
||||||
|
...event,
|
||||||
|
weekPosition: eventPositions.get(event.id) || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Day
|
||||||
|
key={`${weekIndex}-${dayIndex}`}
|
||||||
|
date={date}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
events={getEventsForDay(date)}
|
||||||
|
multiDayEvents={multiDayEvents}
|
||||||
|
dayWidth={dayWidth}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const HEADER_HEIGHT = 40;
|
const HEADER_HEIGHT = 40;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
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: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -302,12 +554,6 @@ const styles = StyleSheet.create({
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
},
|
||||||
day: {
|
|
||||||
height: '15%', // 100% / 6 weeks
|
|
||||||
padding: 4,
|
|
||||||
borderWidth: 0.5,
|
|
||||||
borderColor: '#eee',
|
|
||||||
},
|
|
||||||
weekDay: {
|
weekDay: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -342,19 +588,6 @@ const styles = StyleSheet.create({
|
|||||||
outsideMonthText: {
|
outsideMonthText: {
|
||||||
color: '#ccc',
|
color: '#ccc',
|
||||||
},
|
},
|
||||||
eventsContainer: {
|
|
||||||
flex: 1,
|
|
||||||
marginTop: 2,
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
borderRadius: 4,
|
|
||||||
padding: 2,
|
|
||||||
marginVertical: 1,
|
|
||||||
},
|
|
||||||
eventText: {
|
|
||||||
fontSize: 10,
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
moreEvents: {
|
moreEvents: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"@react-native/assets-registry": "^0.76.3",
|
"@react-native/assets-registry": "^0.76.3",
|
||||||
"@react-navigation/drawer": "^7.0.0",
|
"@react-navigation/drawer": "^7.0.0",
|
||||||
"@react-navigation/native": "^7.0.0",
|
"@react-navigation/native": "^7.0.0",
|
||||||
|
"@shopify/flash-list": "^1.7.2",
|
||||||
"@tanstack/query-async-storage-persister": "^5.62.7",
|
"@tanstack/query-async-storage-persister": "^5.62.7",
|
||||||
"@tanstack/react-query": "^5.62.7",
|
"@tanstack/react-query": "^5.62.7",
|
||||||
"@tanstack/react-query-persist-client": "^5.62.7",
|
"@tanstack/react-query-persist-client": "^5.62.7",
|
||||||
|
15
yarn.lock
15
yarn.lock
@ -3226,6 +3226,14 @@
|
|||||||
component-type "^1.2.1"
|
component-type "^1.2.1"
|
||||||
join-component "^1.1.0"
|
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":
|
"@sideway/address@^4.1.5":
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
|
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"
|
source-map "~0.6.1"
|
||||||
tslib "^2.0.1"
|
tslib "^2.0.1"
|
||||||
|
|
||||||
recyclerlistview@^4.0.0:
|
recyclerlistview@4.2.1, recyclerlistview@^4.0.0:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13"
|
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13"
|
||||||
integrity sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g==
|
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"
|
resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077"
|
||||||
integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==
|
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:
|
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0:
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||||
|
Reference in New Issue
Block a user