Calendar improvements

This commit is contained in:
Milan Paunovic
2025-02-02 22:28:40 +01:00
parent 0998dc29d0
commit 5cfec1544a
22 changed files with 5140 additions and 1188 deletions

View File

@ -20,7 +20,7 @@ const UsersList = () => {
const { user: currentUser } = useAuthContext();
const { data: familyMembers, refetch: refetchFamilyMembers } =
useGetFamilyMembers();
const [selectedUser, setSelectedUser] = useAtom<UserProfile | null>(selectedUserAtom);
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
useEffect(() => {
refetchFamilyMembers();

View File

@ -0,0 +1,27 @@
import React from 'react';
import {useAtomValue} from 'jotai';
import {selectedDateAtom} from '@/components/pages/calendar/atoms';
import {FlashList} from "@shopify/flash-list";
import {useDidUpdate} from "react-native-ui-lib/src/hooks";
interface CalendarControllerProps {
scrollViewRef: React.RefObject<FlashList<any>>;
centerMonthIndex: number;
}
export const CalendarController: React.FC<CalendarControllerProps> = (
{
scrollViewRef,
centerMonthIndex
}) => {
const selectedDate = useAtomValue(selectedDateAtom);
useDidUpdate(() => {
scrollViewRef.current?.scrollToIndex({
index: centerMonthIndex,
animated: false
})
}, [selectedDate, centerMonthIndex]);
return null;
};

View File

@ -1,121 +1,108 @@
import React, {memo, useState} from "react";
import React, {memo, useState, useMemo, useCallback} from "react";
import {StyleSheet} from "react-native";
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
import {MaterialIcons} from "@expo/vector-icons";
import {months} from "./constants";
import {StyleSheet} from "react-native";
import {useAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {format, isSameDay} from "date-fns";
import {format} from "date-fns";
import * as Device from "expo-device";
import {Mode} from "react-native-big-calendar";
import {useIsFetching} from "@tanstack/react-query";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {months} from "./constants";
import RefreshButton from "@/components/shared/RefreshButton";
import {useCalSync} from "@/hooks/useCalSync";
type ViewMode = "day" | "week" | "month" | "3days";
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const SEGMENTS = isTablet
? [{label: "D"}, {label: "W"}, {label: "M"}]
: [{label: "D"}, {label: "3D"}, {label: "M"}];
const MODE_MAP = {
tablet: ["day", "week", "month"],
mobile: ["day", "3days", "month"]
} as const;
export const CalendarHeader = memo(() => {
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const [tempIndex, setTempIndex] = useState<number | null>(null);
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const segments = isTablet
? [{label: "D"}, {label: "W"}, {label: "M"}]
: [{label: "D"}, {label: "3D"}, {label: "M"}];
const {resyncAllCalendars, isSyncing} = useCalSync();
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
const handleSegmentChange = (index: number) => {
let selectedMode: Mode;
if (isTablet) {
selectedMode = ["day", "week", "month"][index] as Mode;
} else {
selectedMode = ["day", "3days", "month"][index] as Mode;
}
const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
const handleSegmentChange = useCallback((index: number) => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
const selectedMode = modes[index] as ViewMode;
setTempIndex(index);
setTimeout(() => {
setMode(selectedMode);
setTempIndex(null);
}, 150);
}, [setMode]);
if (selectedMode) {
setTimeout(() => {
setMode(selectedMode as "day" | "week" | "month" | "3days");
setTempIndex(null);
}, 150);
}
};
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
const handleMonthChange = useCallback((month: string) => {
const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
const updatedDate = new Date(
selectedDate.getFullYear(),
newMonthIndex,
selectedDate.getDate()
);
setSelectedDate(updatedDate);
};
}, [selectedDate, setSelectedDate]);
const isSelectedDateToday = isSameDay(selectedDate, new Date());
const getInitialIndex = () => {
if (isTablet) {
switch (mode) {
case "day":
return 0;
case "week":
return 1;
case "month":
return 2;
default:
return 1;
}
} else {
switch (mode) {
case "day":
return 0;
case "3days":
return 1;
case "month":
return 2;
default:
return 1;
}
const handleRefresh = useCallback(async () => {
try {
await resyncAllCalendars();
} catch (error) {
console.error("Refresh failed:", error);
}
};
}, [resyncAllCalendars]);
const getInitialIndex = useCallback(() => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
//@ts-ignore
return modes.indexOf(mode);
}, [mode]);
const renderMonthPicker = () => (
<View row centerV gap-1 flexS>
{isTablet && (
<Text style={styles.yearText}>
{selectedDate.getFullYear()}
</Text>
)}
<Picker
value={months[selectedDate.getMonth()]}
placeholder="Select Month"
style={styles.monthPicker}
mode={PickerModes.SINGLE}
onChange={value => handleMonthChange(value as string)}
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: styles.yearText,
}}
>
{months.map(month => (
<Picker.Item key={month} label={month} value={month}/>
))}
</Picker>
</View>
);
return (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: isTablet ? 8 : 0,
borderRadius: 20,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: "white",
}}
centerV
>
<View row centerV gap-3>
{isTablet && (
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
{selectedDate.getFullYear()}
</Text>
)}
<Picker
value={months[selectedDate.getMonth()]}
placeholder={"Select Month"}
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
mode={PickerModes.SINGLE}
onChange={(itemValue) => handleMonthChange(itemValue as string)}
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
}}
>
{months.map((month) => (
<Picker.Item key={month} label={month} value={month}/>
))}
</Picker>
</View>
<View style={styles.container} flexG centerV>
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
<View row centerV>
<View row centerV flexS>
<Button
size={"xSmall"}
size="xSmall"
marginR-1
avoidInnerPadding
style={styles.todayButton}
@ -124,27 +111,52 @@ export const CalendarHeader = memo(() => {
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
</Button>
<View>
<View style={styles.segmentContainer}>
<SegmentedControl
segments={segments}
segments={SEGMENTS}
backgroundColor="#ececec"
inactiveColor="#919191"
activeBackgroundColor="#ea156c"
activeColor="white"
outlineColor="white"
outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle}
segmentLabelStyle={styles.segmentLabel}
onChangeIndex={handleSegmentChange}
initialIndex={tempIndex ?? getInitialIndex()}
/>
</View>
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
</View>
</View>
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: isTablet ? 8 : 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
paddingLeft: 10
},
yearText: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
},
monthPicker: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
width: 85,
},
segmentContainer: {
maxWidth: 120,
height: 40,
},
segmentLabel: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},

View File

@ -9,11 +9,6 @@ export default function CalendarPage() {
paddingH-0
paddingT-0
>
{/*<HeaderTemplate
message={"Let's get your week started !"}
isWelcome
isCalendar={true}
/>*/}
<InnerCalendar />
</View>
);

View File

@ -1,21 +1,20 @@
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";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import React, {useCallback, useMemo, useRef, useState} from "react";
import {View} from "react-native-ui-lib";
import {DeviceType} from "expo-device";
import * as Device from "expo-device";
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
import {useAtomValue} from "jotai";
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
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 * as Device from "expo-device"
import {useAtomCallback} from 'jotai/utils'
import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
interface EventCalendarProps {
calendarHeight: number;
calendarWidth: number;
mode: "week" | "month" | "day" | "3days";
onLoad?: () => void;
@ -36,14 +35,14 @@ const MODE_TO_DAYS = {
'3days': 3,
'day': 1,
'month': 1
};
} as const;
const getContainerProps = (selectedDate: Date) => ({
const getContainerProps = (date: Date, customKey: string) => ({
hourWidth: 70,
allowPinchToZoom: true,
useHaptic: true,
scrollToNow: true,
initialDate: selectedDate.toISOString(),
initialDate: customKey !== "default" ? customKey : date.toISOString(),
});
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
@ -51,37 +50,23 @@ const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
prev.event.lastModified === next.event.lastModified;
});
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
{
calendarHeight,
calendarWidth,
mode,
onLoad
}) => {
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
calendarWidth,
mode,
onLoad
}) => {
const {profileData} = useAuthContext();
const selectedDate = useAtomValue(selectedDateAtom);
const {data: familyMembers} = useGetFamilyMembers();
const calendarRef = useRef<CalendarKitHandle>(null);
const {data: events} = useGetEvents();
const selectedUser = useAtomValue(selectedUserAtom);
const calendarRef = useRef<CalendarKitHandle>(null);
const [customKey, setCustomKey] = useState("defaultKey");
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
const containerProps = useMemo(() => getContainerProps(selectedDate), [selectedDate]);
const currentDate = useMemo(() => new Date(), []);
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
const currentMode = get(modeAtom);
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
calendarRef?.current?.goToDate({date: selectedDate});
setCustomKey(selectedDate.toISOString())
}
}, [selectedDate]));
useEffect(() => {
checkModeAndGoToDate();
}, [selectedDate, checkModeAndGoToDate]);
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
const {
handlePressEvent,
handlePressCell,
@ -115,9 +100,12 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
onPressEvent={handlePressEvent}
onPressBackground={handlePressCell}
onLoad={onLoad}
key={customKey}
>
<CalendarHeader {...HEADER_PROPS} />
<DetailedCalendarController
calendarRef={calendarRef}
setCustomKey={setCustomKey}
/>
<CalendarHeader {...HEADER_PROPS}/>
<CalendarBody
{...BODY_PROPS}
renderEvent={renderEvent}

View File

@ -0,0 +1,38 @@
import React, {useCallback, useEffect, useRef} from 'react';
import {useAtomValue} from 'jotai';
import {useAtomCallback} from 'jotai/utils';
import {modeAtom, selectedDateAtom} from '@/components/pages/calendar/atoms';
import {isToday} from 'date-fns';
import {CalendarKitHandle} from "@howljs/calendar-kit";
interface DetailedCalendarDateControllerProps {
calendarRef: React.RefObject<CalendarKitHandle>;
setCustomKey: (key: string) => void;
}
export const DetailedCalendarController: React.FC<DetailedCalendarDateControllerProps> = ({
calendarRef,
setCustomKey
}) => {
const selectedDate = useAtomValue(selectedDateAtom);
const lastSelectedDate = useRef(selectedDate);
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
const currentMode = get(modeAtom);
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
if (currentMode === "month") {
setCustomKey(selectedDate.toISOString());
}
calendarRef?.current?.goToDate({date: selectedDate});
}
}, [selectedDate, calendarRef, setCustomKey]));
useEffect(() => {
if (selectedDate !== lastSelectedDate.current) {
checkModeAndGoToDate();
lastSelectedDate.current = selectedDate;
}
}, [selectedDate, checkModeAndGoToDate]);
return null;
};

View File

@ -1,33 +1,31 @@
import React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native';
import { Text } from 'react-native-ui-lib';
import {StyleSheet, View, ActivityIndicator} from 'react-native';
import {Text} from 'react-native-ui-lib';
import Animated, {
withTiming,
useAnimatedStyle,
FadeOut,
useSharedValue,
runOnJS
} from 'react-native-reanimated';
import { useGetEvents } from '@/hooks/firebase/useGetEvents';
import { useCalSync } from '@/hooks/useCalSync';
import { useSyncEvents } from '@/hooks/useSyncOnScroll';
import { useAtom } from 'jotai';
import { modeAtom } from './atoms';
import { MonthCalendar } from "@/components/pages/calendar/MonthCalendar";
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
import {useCalSync} from '@/hooks/useCalSync';
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
import {useAtom} from 'jotai';
import {modeAtom} from './atoms';
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
import * as Device from "expo-device";
export type CalendarMode = 'month' | 'day' | '3days' | 'week';
interface EventCalendarProps {
calendarHeight: number;
calendarWidth: number;
}
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
const { isLoading } = useGetEvents();
const {isLoading} = useGetEvents();
const [mode] = useAtom<CalendarMode>(modeAtom);
const { isSyncing } = useSyncEvents();
const {isSyncing} = useSyncEvents();
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const isCalendarReady = useSharedValue(false);
useCalSync();
@ -37,26 +35,26 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
}, []);
const containerStyle = useAnimatedStyle(() => ({
opacity: withTiming(isCalendarReady.value ? 1 : 0, { duration: 500 }),
opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
flex: 1,
}));
const monthStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'month' ? 1 : 0, { duration: 300 }),
opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
}));
const detailedDayStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'day' ? 1 : 0, { duration: 300 }),
opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
}));
const detailedMultiStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, { duration: 300 }),
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, {duration: 300}),
position: 'absolute',
width: '100%',
height: '100%',
@ -64,7 +62,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
return (
<View style={styles.root}>
{(isLoading || isSyncing) && mode !== 'month' && (
{(isLoading || isSyncing) && mode !== 'month' && (
<Animated.View
exiting={FadeOut.duration(300)}
style={styles.loadingContainer}
@ -75,14 +73,19 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
)}
<Animated.View style={containerStyle}>
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
<MonthCalendar {...props} />
<MonthCalendar/>
</Animated.View>
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
<DetailedCalendar mode="day" {...props} />
</Animated.View>
<Animated.View style={detailedMultiStyle} pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
<Animated.View style={detailedMultiStyle}
pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
{!isLoading && (
<DetailedCalendar onLoad={handleRenderComplete} mode={isTablet ? 'week' : '3days'} {...props} />
<DetailedCalendar
onLoad={handleRenderComplete}
mode={isTablet ? 'week' : '3days'}
{...props}
/>
)}
</Animated.View>
</Animated.View>

View File

@ -4,19 +4,16 @@ import {LayoutChangeEvent} from "react-native";
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
export const InnerCalendar = () => {
const [calendarHeight, setCalendarHeight] = useState(0);
const [calendarWidth, setCalendarWidth] = useState(0);
const calendarContainerRef = useRef(null);
const hasSetInitialSize = useRef(false);
const onLayout = useCallback((event: LayoutChangeEvent) => {
if (!hasSetInitialSize.current) {
const {height, width} = event.nativeEvent.layout;
setCalendarHeight(height);
const width = event.nativeEvent.layout.width;
setCalendarWidth(width);
hasSetInitialSize.current = true;
}
@ -30,12 +27,9 @@ export const InnerCalendar = () => {
onLayout={onLayout}
paddingB-0
>
{calendarHeight > 0 && (
<EventCalendar
calendarHeight={calendarHeight}
calendarWidth={calendarWidth}
/>
)}
<EventCalendar
calendarWidth={calendarWidth}
/>
</View>
<CalendarViewSwitch/>

View File

@ -122,7 +122,7 @@ export const ManuallyAddEventModal = () => {
const {
mutateAsync: createEvent,
isLoading: isAdding,
isPending: isAdding,
isError,
} = useCreateEvent();
const {data: members} = useGetFamilyMembers(true);
@ -178,15 +178,6 @@ export const ManuallyAddEventModal = () => {
if (!show) return null;
const formatDateTime = (date?: Date | string) => {
if (!date) return undefined;
return new Date(date).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
});
};
const showDeleteEventModal = () => {
setDeleteModalVisible(true);
};
@ -257,38 +248,6 @@ export const ManuallyAddEventModal = () => {
return true;
};
const getRepeatLabel = () => {
const selectedDays = repeatInterval;
const allDays = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
if (isEveryDay) {
return "Every day";
} else if (
isEveryWorkDay &&
!selectedDays.includes("saturday") &&
!selectedDays.includes("sunday")
) {
return "Every work day";
} else {
return selectedDays
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
.join(", ");
}
};
if (isLoading && !isError) {
return (
<Modal
@ -355,7 +314,7 @@ export const ManuallyAddEventModal = () => {
setDate(newDate);
if(isStart) {
if (startTime.getHours() > endTime.getHours() &&
if (startTime.getHours() > endTime.getHours() &&
(isSameDay(newDate, endDate) || isAfter(newDate, endDate))) {
const newEndDate = new Date(newDate);
newEndDate.setDate(newEndDate.getDate() + 1);
@ -364,7 +323,7 @@ export const ManuallyAddEventModal = () => {
setEndDate(newEndDate);
}
} else {
if (endTime.getHours() < startTime.getHours() &&
if (endTime.getHours() < startTime.getHours() &&
(isSameDay(newDate, startDate) || isAfter(startDate, newDate))) {
const newStartDate = new Date(newDate);
newStartDate.setDate(newStartDate.getDate() - 1);
@ -432,7 +391,7 @@ export const ManuallyAddEventModal = () => {
is24Hour={profileData?.userType === ProfileType.PARENT ? false : true}
onChange={(time) => {
if (endDate.getDate() === startDate.getDate() &&
time.getHours() >= endTime.getHours() && time.getMinutes() >= endTime.getHours())
time.getHours() >= endTime.getHours() && time.getMinutes() >= endTime.getHours())
{
const newEndDate = new Date(endDate);
@ -801,9 +760,9 @@ export const ManuallyAddEventModal = () => {
<CameraIcon color="white"/>
</View>
)}
/>
/>
{editEvent && (
<TouchableOpacity
<TouchableOpacity
onPress={showDeleteEventModal}
style={{ marginTop: 15, marginBottom: 40, alignSelf: "center" }}
hitSlop={{left: 30, right: 30, top: 10, bottom: 10}}

View File

@ -1,30 +1,23 @@
import React, {useCallback, useEffect, useMemo, useRef, useState, useTransition} from 'react';
import {
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native';
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,
addMonths, startOfWeek, isWithinInterval,
} from 'date-fns';
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai";
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
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 debounce from "debounce";
import {CalendarController} from "@/components/pages/calendar/CalendarController";
interface CalendarEvent {
id: string;
@ -40,12 +33,12 @@ interface CustomMonthCalendarProps {
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MAX_VISIBLE_EVENTS = 3;
const CENTER_MONTH_INDEX = 24;
const CENTER_MONTH_INDEX = 12;
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
<TouchableOpacity
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]}
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
onPress={onPress}
>
<Text style={styles.eventText} numberOfLines={1}>
@ -78,7 +71,7 @@ const MultiDayEvent = React.memo(({
const style = {
position: 'absolute' as const,
height: 14,
backgroundColor: event.color || '#6200ee',
backgroundColor: event?.eventColor || '#6200ee',
padding: 2,
zIndex: 1,
left: isStart ? 4 : -0.5, // Extend slightly into the border
@ -103,22 +96,21 @@ const MultiDayEvent = React.memo(({
);
});
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 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);
@ -126,7 +118,6 @@ const Day = React.memo(({
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
// Calculate space needed for multi-day events
const maxMultiDayPosition = multiDayEvents.length > 0
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
: 0;
@ -140,7 +131,7 @@ const Day = React.memo(({
>
<View style={[
styles.dateContainer,
isToday && styles.todayContainer,
isToday && {backgroundColor: events?.[0]?.eventColor},
]}>
<Text style={[
styles.dateText,
@ -185,18 +176,9 @@ const Day = React.memo(({
);
});
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 setSelectedDate = useSetAtom(selectedDateAtom);
const setMode = useSetAtom(modeAtom);
const {profileData} = useAuthContext();
@ -204,10 +186,8 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
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 isScrolling = useRef(false);
const lastScrollUpdate = useRef<Date>(new Date());
const dayWidth = screenWidth / 7;
const centerMonth = useRef(new Date());
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
@ -255,10 +235,10 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
});
}
return months;
}, [getMonthData]);
}, [getMonthData, rawEvents]);
const processedEvents = useMemo(() => {
if (!rawEvents?.length || !selectedDate) return {
if (!rawEvents?.length) return {
eventMap: new Map(),
multiDayEvents: []
};
@ -324,7 +304,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
<Month
date={item.date}
days={item.days}
selectedDate={selectedDate}
getEventsForDay={getEventsForDay}
getMultiDayEventsForDay={getMultiDayEventsForDay}
dayWidth={dayWidth}
@ -334,31 +313,12 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
/>
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
const debouncedSetSelectedDate = useMemo(
() => debounce(setSelectedDate, 500),
[setSelectedDate]
);
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (isScrolling.current) return;
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]);
useEffect(() => {
return () => {
debouncedSetSelectedDate.clear();
};
}, [debouncedSetSelectedDate]);
return (
<View style={styles.container}>
<CalendarController
scrollViewRef={scrollViewRef}
centerMonthIndex={CENTER_MONTH_INDEX}
/>
<FlashList
ref={scrollViewRef}
data={monthsToRender}
@ -368,7 +328,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
pagingEnabled
showsHorizontalScrollIndicator={false}
initialScrollIndex={CENTER_MONTH_INDEX}
onScroll={onScroll}
removeClippedSubviews={true}
estimatedItemSize={screenWidth}
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
@ -391,7 +350,6 @@ const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
const Month = React.memo(({
date,
days,
selectedDate,
getEventsForDay,
getMultiDayEventsForDay,
dayWidth,
@ -401,7 +359,6 @@ const Month = React.memo(({
}: {
date: Date;
days: Date[];
selectedDate: Date;
getEventsForDay: (date: Date) => CalendarEvent[];
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
dayWidth: number;
@ -481,7 +438,6 @@ const Month = React.memo(({
<Day
key={`${weekIndex}-${dayIndex}`}
date={date}
selectedDate={selectedDate}
events={getEventsForDay(date)}
multiDayEvents={multiDayEvents}
dayWidth={dayWidth}
@ -529,11 +485,11 @@ const styles = StyleSheet.create({
},
day: {
height: '14%',
padding: 0, // Remove padding
padding: 0,
borderWidth: 0.5,
borderColor: '#eee',
position: 'relative',
overflow: 'visible', // Allow events to overflow
overflow: 'visible',
},
container: {
flex: 1,
@ -555,7 +511,7 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center'
// justifyContent: 'center'
},
weekDay: {
alignItems: 'center',
@ -609,6 +565,8 @@ const styles = StyleSheet.create({
weekDayRow: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 16,
paddingHorizontal: 0,
},
});
});
export default MonthCalendar;

View File

@ -46,7 +46,7 @@ const createEventHash = (event: FormattedEvent): string => {
// Precompute time constants
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const PERIOD_IN_MS = 5 * DAY_IN_MS;
const PERIOD_IN_MS = 180 * DAY_IN_MS;
const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Memoize date range calculations

View File

@ -52,7 +52,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
addChoreDialogProps.selectedTodo ?? defaultTodo
);
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid]
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid!]
);
const {width} = Dimensions.get("screen");
const [points, setPoints] = useState<number>(todo.points);

View File

@ -1,6 +1,6 @@
import React, { useRef, useEffect } from 'react';
import { TouchableOpacity, Animated, Easing } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { Feather } from '@expo/vector-icons';
interface RefreshButtonProps {
onRefresh: () => Promise<void>;
@ -9,12 +9,12 @@ interface RefreshButtonProps {
color?: string;
}
const RefreshButton = ({
onRefresh,
isSyncing,
size = 24,
color = "#83807F"
}: RefreshButtonProps) => {
const RefreshButton = ({
onRefresh,
isSyncing,
size = 24,
color = "#83807F"
}: RefreshButtonProps) => {
const rotateAnim = useRef(new Animated.Value(0)).current;
const rotationLoop = useRef<Animated.CompositeAnimation | null>(null);
@ -29,12 +29,12 @@ const RefreshButton = ({
const startContinuousRotation = () => {
rotateAnim.setValue(0);
rotationLoop.current = Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
})
Animated.timing(rotateAnim, {
toValue: 1,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true,
})
);
rotationLoop.current.start();
};
@ -56,11 +56,28 @@ const RefreshButton = ({
};
return (
<TouchableOpacity onPress={handlePress} disabled={isSyncing}>
<Animated.View style={{ transform: [{ rotate }] }}>
<Feather name="refresh-cw" size={size} color={color} />
</Animated.View>
</TouchableOpacity>
<TouchableOpacity
onPress={handlePress}
disabled={isSyncing}
style={{
width: size * 2,
height: size + 10,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Animated.View
style={{
transform: [{ rotate }],
width: size,
height: size,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Feather name="refresh-cw" size={size} color={color} />
</Animated.View>
</TouchableOpacity>
);
};