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

12
.idea/material_theme_project_new.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-7d3b2185:193a8bd7023:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

View File

@ -1,433 +1,356 @@
import React from "react"; import React, {useCallback} from "react";
import { Drawer } from "expo-router/drawer"; import {Drawer} from "expo-router/drawer";
import { useSignOut } from "@/hooks/firebase/useSignOut"; import {DrawerContentScrollView, DrawerContentComponentProps, DrawerNavigationOptions} from "@react-navigation/drawer";
import { import {ImageBackground, Pressable, StyleSheet} from "react-native";
DrawerContentScrollView, import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
DrawerNavigationOptions, import * as Device from "expo-device";
DrawerNavigationProp, import {useSetAtom} from "jotai";
} from "@react-navigation/drawer"; import {Ionicons} from "@expo/vector-icons";
import { import {DeviceType} from "expo-device";
Button, import {DrawerNavigationProp} from "@react-navigation/drawer";
ButtonSize, import {ParamListBase, Theme} from '@react-navigation/native';
Text, import {RouteProp} from "@react-navigation/native";
TouchableOpacity,
View, import {useSignOut} from "@/hooks/firebase/useSignOut";
} from "react-native-ui-lib"; import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
import { ImageBackground, Pressable, StyleSheet } from "react-native";
import DrawerButton from "@/components/shared/DrawerButton"; import DrawerButton from "@/components/shared/DrawerButton";
import DrawerIcon from "@/assets/svgs/DrawerIcon";
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon"; import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon"; import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon"; import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon"; import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon"; import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
import { useSetAtom } from "jotai";
import {
isFamilyViewAtom,
settingsPageIndex,
toDosPageIndex,
userSettingsView,
} from "@/components/pages/calendar/atoms";
import Ionicons from "@expo/vector-icons/Ionicons";
import * as Device from "expo-device";
import { DeviceType } from "expo-device";
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon"; import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
import DrawerIcon from "@/assets/svgs/DrawerIcon"; import {
import { RouteProp } from "@react-navigation/core"; isFamilyViewAtom,
import RefreshButton from "@/components/shared/RefreshButton"; settingsPageIndex,
import { useCalSync } from "@/hooks/useCalSync"; toDosPageIndex,
import {useIsFetching} from "@tanstack/react-query"; userSettingsView,
import { CalendarHeader } from "@/components/pages/calendar/CalendarHeader"; } from "@/components/pages/calendar/atoms";
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
const isTablet = Device.deviceType === DeviceType.TABLET;
type DrawerParamList = { type DrawerParamList = {
index: undefined; index: undefined;
calendar: undefined; calendar: undefined;
todos: undefined; brain_dump: undefined;
settings: undefined;
grocery: undefined;
reminders: undefined;
todos: undefined;
notifications: undefined;
feedback: undefined;
}; };
type NavigationProp = DrawerNavigationProp<DrawerParamList>; type DrawerScreenNavigationProp = DrawerNavigationProp<DrawerParamList>;
interface ViewSwitchProps { interface DrawerButtonConfig {
navigation: NavigationProp; id: string;
title: string;
color: string;
bgColor: string;
icon: React.FC;
route: keyof DrawerParamList;
} }
const DRAWER_BUTTONS: DrawerButtonConfig[] = [
{
id: 'calendar',
title: 'Calendar',
color: 'rgb(7, 184, 199)',
bgColor: 'rgb(231, 248, 250)',
icon: NavCalendarIcon,
route: 'calendar'
},
{
id: 'grocery',
title: 'Groceries',
color: '#50be0c',
bgColor: '#eef9e7',
icon: NavGroceryIcon,
route: 'grocery'
},
{
id: 'feedback',
title: 'Feedback',
color: '#ea156d',
bgColor: '#fdedf4',
icon: FeedbackNavIcon,
route: 'feedback'
},
{
id: 'todos',
title: 'To Dos',
color: '#8005eb',
bgColor: '#f3e6fd',
icon: NavToDosIcon,
route: 'todos'
},
{
id: 'brain_dump',
title: 'Brain Dump',
color: '#e0ca03',
bgColor: '#fffacb',
icon: NavBrainDumpIcon,
route: 'brain_dump'
},
{
id: 'notifications',
title: 'Notifications',
color: '#ffa200',
bgColor: '#ffdda1',
icon: () => <Ionicons name="notifications-outline" size={24} color="#ffa200"/>,
route: 'notifications'
}
];
interface DrawerContentProps {
props: DrawerContentComponentProps;
}
const DrawerContent: React.FC<DrawerContentProps> = ({props}) => {
const {mutateAsync: signOut} = useSignOut();
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
const setPageIndex = useSetAtom(settingsPageIndex);
const setUserView = useSetAtom(userSettingsView);
const setToDosIndex = useSetAtom(toDosPageIndex);
const handleNavigation = useCallback((route: keyof DrawerParamList) => {
props.navigation.navigate(route);
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}, [props.navigation, setPageIndex, setToDosIndex, setUserView, setIsFamilyView]);
const renderDrawerButtons = () => {
const midPoint = Math.ceil(DRAWER_BUTTONS.length / 2);
const leftButtons = DRAWER_BUTTONS.slice(0, midPoint);
const rightButtons = DRAWER_BUTTONS.slice(midPoint);
return (
<View row paddingH-30>
<View flex-1 paddingR-5>
{leftButtons.map(button => (
<DrawerButton
key={button.id}
title={button.title}
color={button.color}
bgColor={button.bgColor}
pressFunc={() => handleNavigation(button.route)}
icon={<button.icon/>}
/>
))}
</View>
<View flex-1>
{rightButtons.map(button => (
<DrawerButton
key={button.id}
title={button.title}
color={button.color}
bgColor={button.bgColor}
pressFunc={() => handleNavigation(button.route)}
icon={<button.icon/>}
/>
))}
</View>
</View>
);
};
return (
<DrawerContentScrollView {...props}>
<View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground
source={require("../../assets/images/splash.png")}
style={styles.logo}
/>
<Text style={styles.title}>Welcome to Cally</Text>
</View>
{renderDrawerButtons()}
<Button
onPress={() => handleNavigation('settings')}
label="Manage Settings"
labelStyle={styles.label}
iconSource={() => (
<View style={styles.settingsIcon}>
<NavSettingsIcon/>
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
borderRadius={18.55}
style={{elevation: 0}}
/>
<Button
size={ButtonSize.large}
style={styles.signOutButton}
label="Sign out of Cally"
color="#fd1775"
labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
};
interface HeaderRightProps { interface HeaderRightProps {
routeName: keyof DrawerParamList; route: RouteProp<DrawerParamList>;
navigation: NavigationProp; navigation: DrawerScreenNavigationProp;
} }
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({ navigation }) => ( const HeaderRight: React.FC<HeaderRightProps> = ({route, navigation}) => {
<ViewSwitch navigation={navigation} /> const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
)); const isCalendarPage = ["calendar", "index"].includes(route.name);
const HeaderRight = React.memo<HeaderRightProps>( if (!isTablet || !showViewSwitch) {
({ routeName, navigation }) => { return isCalendarPage ? <CalendarHeader/> : null;
const showViewSwitch = ["calendar", "todos", "index"].includes(routeName);
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
return null;
} }
return <MemoizedViewSwitch navigation={navigation} />; return (
} <View marginR-16 row centerV>
); {isTablet && isCalendarPage && (
export default function TabLayout() { <View flex-1 center>
const { mutateAsync: signOut } = useSignOut(); <CalendarHeader/>
const setIsFamilyView = useSetAtom(isFamilyViewAtom); </View>
const setPageIndex = useSetAtom(settingsPageIndex); )}
const setUserView = useSetAtom(userSettingsView); <ViewSwitch navigation={navigation}/>
const setToDosIndex = useSetAtom(toDosPageIndex); </View>
const { resyncAllCalendars, isSyncing } = useCalSync(); );
};
const isFetching = useIsFetching({queryKey: ['events']}) > 0; const screenOptions: (props: {
route: RouteProp<ParamListBase, string>;
const isLoading = React.useMemo(() => { navigation: DrawerNavigationProp<ParamListBase, string>;
return isSyncing || isFetching; theme: Theme;
}, [isSyncing, isFetching]); }) => DrawerNavigationOptions = ({route, navigation}) => ({
const onRefresh = React.useCallback(async () => {
try {
await resyncAllCalendars();
} catch (error) {
console.error("Refresh failed:", error);
}
}, [resyncAllCalendars]);
const screenOptions = ({
navigation,
route,
}: {
navigation: DrawerNavigationProp<DrawerParamList>;
route: RouteProp<DrawerParamList>;
}): DrawerNavigationOptions => ({
lazy: true, lazy: true,
headerShown: true, headerShown: true,
headerTitleAlign: headerTitleAlign: "left",
Device.deviceType === DeviceType.TABLET ? "left" : "unaligned", headerTitle: ({children}) => {
headerTitle: ({ children }) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name); const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute) return null;
if (isCalendarRoute && Device.deviceType !== DeviceType.TABLET) {
return <View centerV><CalendarHeader /></View>;
}
return ( return (
<Text <View flexG centerV paddingL-10>
style={{ <Text style={styles.headerTitle}>
fontFamily: "Manrope_600SemiBold", {children}
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17, </Text>
}} </View>
>
{children}
</Text>
); );
},
headerTitleStyle: {
fontFamily: "Manrope_600SemiBold",
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
}, },
headerLeft: () => ( headerLeft: () => (
<Pressable <Pressable
onPress={() => { onPress={() => navigation.toggleDrawer()}
navigation.toggleDrawer() hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
}} style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} >
style={({ pressed }) => [ <DrawerIcon/>
{ </Pressable>
marginLeft: 16,
opacity: pressed ? 0.4 : 1
}
]}
>
<DrawerIcon />
</Pressable>
), ),
headerRight: () => { headerRight: () => <HeaderRight
const showViewSwitch = ["calendar", "todos", "index"].includes( route={route as RouteProp<DrawerParamList>}
route.name navigation={navigation as DrawerNavigationProp<DrawerParamList>}
); />,
const isCalendarPage = ["calendar", "index"].includes(route.name); drawerStyle: styles.drawer,
});
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) { interface DrawerScreen {
return isCalendarPage ? ( name: keyof DrawerParamList;
<View marginR-8 > title: string;
<RefreshButton onRefresh={onRefresh} isSyncing={isLoading} /> hideInDrawer?: boolean;
</View>
) : null;
}
return (
<View marginR-16 row centerV>
{Device.deviceType === DeviceType.TABLET && isCalendarPage && <View flex-1 center><CalendarHeader />
</View>}
{isCalendarPage && (
<View marginR-16><RefreshButton onRefresh={onRefresh} isSyncing={isLoading} /></View>
)}
<MemoizedViewSwitch navigation={navigation} />
</View>
);
},
drawerStyle: {
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
backgroundColor: "#f9f8f7",
height: "100%",
},
});
return (
<Drawer
initialRouteName={"index"}
detachInactiveScreens
screenOptions={screenOptions}
drawerContent={(props) => {
return (
<DrawerContentScrollView {...props} style={{}}>
<View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground
source={require("../../assets/images/splash.png")}
style={{
backgroundColor: "transparent",
height: 51.43,
aspectRatio: 1,
marginRight: 8,
}}
/>
<Text style={styles.title}>Welcome to Cally</Text>
</View>
<View
style={{
flexDirection: "row",
paddingHorizontal: 30,
}}
>
<View style={{ flex: 1, paddingRight: 5 }}>
<DrawerButton
title={"Calendar"}
color="rgb(7, 184, 199)"
bgColor={"rgb(231, 248, 250)"}
pressFunc={() => {
props.navigation.navigate("calendar");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavCalendarIcon />}
/>
<DrawerButton
color="#50be0c"
title={"Groceries"}
bgColor={"#eef9e7"}
pressFunc={() => {
props.navigation.navigate("grocery");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavGroceryIcon />}
/>
<DrawerButton
color="#ea156d"
title={"Feedback"}
bgColor={"#fdedf4"}
pressFunc={() => {
props.navigation.navigate("feedback");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<FeedbackNavIcon />}
/>
</View>
<View style={{ flex: 1, paddingRight: 0 }}>
{/*<DrawerButton
color="#fd1775"
title={"My Reminders"}
bgColor={"#ffe8f2"}
pressFunc={() => props.navigation.navigate("reminders")}
icon={
<FontAwesome6
name="clock-rotate-left"
size={28}
color="#fd1775"
/>
}
/>*/}
<DrawerButton
color="#8005eb"
title={"To Dos"}
bgColor={"#f3e6fd"}
pressFunc={() => {
props.navigation.navigate("todos");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavToDosIcon />}
/>
<DrawerButton
color="#e0ca03"
title={"Brain Dump"}
bgColor={"#fffacb"}
pressFunc={() => {
props.navigation.navigate("brain_dump");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={<NavBrainDumpIcon />}
/>
<DrawerButton
color="#e0ca03"
title={"Notifications"}
bgColor={"#ffdda1"}
pressFunc={() => {
props.navigation.navigate("notifications");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
icon={
<Ionicons
name="notifications-outline"
size={24}
color={"#ffa200"}
/>
}
/>
</View>
</View>
<Button
onPress={() => {
props.navigation.navigate("settings");
setPageIndex(0);
setToDosIndex(0);
setUserView(true);
setIsFamilyView(false);
}}
label={"Manage Settings"}
labelStyle={styles.label}
iconSource={() => (
<View
backgroundColor="#ededed"
width={60}
height={60}
style={{ borderRadius: 50 }}
marginR-10
centerV
centerH
>
<NavSettingsIcon />
</View>
)}
backgroundColor="white"
color="#464039"
paddingV-30
marginH-30
borderRadius={18.55}
style={{ elevation: 0 }}
/>
<Button
size={ButtonSize.large}
marginH-10
marginT-12
paddingV-15
style={{
marginTop: 50,
backgroundColor: "transparent",
borderWidth: 1.3,
borderColor: "#fd1775",
}}
label="Sign out of Cally"
color="#fd1775"
labelStyle={styles.signOut}
onPress={() => signOut()}
/>
</DrawerContentScrollView>
);
}}
>
<Drawer.Screen
name="index"
options={{
drawerLabel: "Calendar",
title: "Calendar",
}}
/>
<Drawer.Screen
name="calendar"
options={{
drawerLabel: "Calendar",
title: "Calendar",
drawerItemStyle: { display: "none" },
}}
/>
<Drawer.Screen
name="brain_dump"
options={{
drawerLabel: "Brain Dump",
title: "Brain Dump",
}}
/>
<Drawer.Screen
name="settings"
options={{
drawerLabel: "Settings",
title: "Settings",
}}
/>
<Drawer.Screen
name="grocery"
options={{
drawerLabel: "Groceries",
title: "Groceries",
}}
/>
<Drawer.Screen
name="reminders"
options={{
drawerLabel: "Reminders",
title: "Reminders",
}}
/>
<Drawer.Screen
name="todos"
options={{
drawerLabel: "To-Do",
title:
Device.deviceType === DeviceType.TABLET
? "Family To Dos"
: "To Dos",
}}
/>
<Drawer.Screen
name="notifications"
options={{
drawerLabel: "Notifications",
title: "Notifications",
}}
/>
<Drawer.Screen
name="feedback"
options={{ drawerLabel: "Feedback", title: "Feedback" }}
/>
</Drawer>
);
} }
const DRAWER_SCREENS: DrawerScreen[] = [
{name: 'index', title: 'Calendar'},
{name: 'calendar', title: 'Calendar', hideInDrawer: true},
{name: 'brain_dump', title: 'Brain Dump'},
{name: 'settings', title: 'Settings'},
{name: 'grocery', title: 'Groceries'},
{name: 'reminders', title: 'Reminders'},
{name: 'todos', title: isTablet ? 'Family To Dos' : 'To Dos'},
{name: 'notifications', title: 'Notifications'},
{name: 'feedback', title: 'Feedback'}
];
const TabLayout: React.FC = () => {
return (
<Drawer
initialRouteName="index"
detachInactiveScreens
screenOptions={screenOptions}
drawerContent={(props) => <DrawerContent props={props}/>}
>
{DRAWER_SCREENS.map(screen => (
<Drawer.Screen
key={screen.name}
name={screen.name}
options={{
drawerLabel: screen.title,
title: screen.title,
...(screen.hideInDrawer && {drawerItemStyle: {display: 'none'}}),
}}
/>
))}
</Drawer>
);
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 }, drawer: {
label: { fontFamily: "Poppins_400Medium", fontSize: 15 }, width: isTablet ? "30%" : "90%",
title: { backgroundColor: "#f9f8f7",
fontSize: 26.13, height: "100%",
fontFamily: "Manrope_600SemiBold", },
color: "#262627", drawerTrigger: {
}, marginLeft: 16,
},
headerTitle: {
fontFamily: "Manrope_600SemiBold",
fontSize: isTablet ? 22 : 17,
},
logo: {
backgroundColor: "transparent",
height: 51.43,
aspectRatio: 1,
marginRight: 8,
},
settingsIcon: {
backgroundColor: "#ededed",
width: 60,
height: 60,
borderRadius: 50,
marginRight: 10,
justifyContent: 'center',
alignItems: 'center',
},
signOutButton: {
marginTop: 50,
marginHorizontal: 10,
paddingVertical: 15,
backgroundColor: "transparent",
borderWidth: 1.3,
borderColor: "#fd1775",
},
signOut: {
fontFamily: "Poppins_500Medium",
fontSize: 15,
},
label: {
fontFamily: "Poppins_400Medium",
fontSize: 15,
},
title: {
fontSize: 26.13,
fontFamily: "Manrope_600SemiBold",
color: "#262627",
},
}); });
export default TabLayout;

View File

@ -50,15 +50,14 @@ import {Stack} from "expo-router";
import * as SplashScreen from "expo-splash-screen"; import * as SplashScreen from "expo-splash-screen";
import "react-native-reanimated"; import "react-native-reanimated";
import {AuthContextProvider} from "@/contexts/AuthContext"; import {AuthContextProvider} from "@/contexts/AuthContext";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib"; import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
import {Platform} from 'react-native'; import {Platform} from 'react-native';
import KeyboardManager from 'react-native-keyboard-manager'; import KeyboardManager from 'react-native-keyboard-manager';
import {enableScreens} from 'react-native-screens'; import {enableScreens, enableFreeze} from 'react-native-screens';
import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider"; import {PersistQueryClientProvider} from "@/contexts/PersistQueryClientProvider";
import auth from "@react-native-firebase/auth";
enableScreens(true) enableScreens(true)
enableFreeze(true)
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();

View File

@ -20,7 +20,7 @@ const UsersList = () => {
const { user: currentUser } = useAuthContext(); const { user: currentUser } = useAuthContext();
const { data: familyMembers, refetch: refetchFamilyMembers } = const { data: familyMembers, refetch: refetchFamilyMembers } =
useGetFamilyMembers(); useGetFamilyMembers();
const [selectedUser, setSelectedUser] = useAtom<UserProfile | null>(selectedUserAtom); const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
useEffect(() => { useEffect(() => {
refetchFamilyMembers(); 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 {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 {StyleSheet} from "react-native";
import {useAtom} from "jotai"; import {useAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms"; import {format} from "date-fns";
import {format, isSameDay} from "date-fns";
import * as Device from "expo-device"; 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(() => { 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 [tempIndex, setTempIndex] = useState<number | null>(null);
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const segments = isTablet const {resyncAllCalendars, isSyncing} = useCalSync();
? [{label: "D"}, {label: "W"}, {label: "M"}] const isFetching = useIsFetching({queryKey: ['events']}) > 0;
: [{label: "D"}, {label: "3D"}, {label: "M"}];
const handleSegmentChange = (index: number) => { const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
let selectedMode: Mode;
if (isTablet) { const handleSegmentChange = useCallback((index: number) => {
selectedMode = ["day", "week", "month"][index] as Mode; const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
} else { const selectedMode = modes[index] as ViewMode;
selectedMode = ["day", "3days", "month"][index] as Mode;
}
setTempIndex(index); setTempIndex(index);
setTimeout(() => {
setMode(selectedMode);
setTempIndex(null);
}, 150);
}, [setMode]);
if (selectedMode) { const handleMonthChange = useCallback((month: string) => {
setTimeout(() => {
setMode(selectedMode as "day" | "week" | "month" | "3days");
setTempIndex(null);
}, 150);
}
};
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
const newMonthIndex = months.indexOf(month); const newMonthIndex = months.indexOf(month);
const updatedDate = new Date(
const updatedDate = new Date(currentYear, newMonthIndex, currentDay); selectedDate.getFullYear(),
newMonthIndex,
selectedDate.getDate()
);
setSelectedDate(updatedDate); setSelectedDate(updatedDate);
}; }, [selectedDate, setSelectedDate]);
const isSelectedDateToday = isSameDay(selectedDate, new Date()); const handleRefresh = useCallback(async () => {
try {
const getInitialIndex = () => { await resyncAllCalendars();
if (isTablet) { } catch (error) {
switch (mode) { console.error("Refresh failed:", error);
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;
}
} }
}; }, [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 ( return (
<View <View style={styles.container} flexG centerV>
style={{ {mode !== "month" ? renderMonthPicker() : <View flexG/>}
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 row centerV> <View row centerV flexS>
<Button <Button
size={"xSmall"} size="xSmall"
marginR-1 marginR-1
avoidInnerPadding avoidInnerPadding
style={styles.todayButton} style={styles.todayButton}
@ -124,27 +111,52 @@ export const CalendarHeader = memo(() => {
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/> <MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text> <Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
</Button> </Button>
<View>
<View style={styles.segmentContainer}>
<SegmentedControl <SegmentedControl
segments={segments} segments={SEGMENTS}
backgroundColor="#ececec" backgroundColor="#ececec"
inactiveColor="#919191" inactiveColor="#919191"
activeBackgroundColor="#ea156c" activeBackgroundColor="#ea156c"
activeColor="white" activeColor="white"
outlineColor="white" outlineColor="white"
outlineWidth={3} outlineWidth={3}
segmentLabelStyle={styles.segmentslblStyle} segmentLabelStyle={styles.segmentLabel}
onChangeIndex={handleSegmentChange} onChangeIndex={handleSegmentChange}
initialIndex={tempIndex ?? getInitialIndex()} initialIndex={tempIndex ?? getInitialIndex()}
/> />
</View> </View>
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
</View> </View>
</View> </View>
); );
}); });
const styles = StyleSheet.create({ 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, fontSize: 12,
fontFamily: "Manrope_600SemiBold", fontFamily: "Manrope_600SemiBold",
}, },

View File

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

View File

@ -1,21 +1,20 @@
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; import React, {useCallback, useMemo, useRef, useState} from "react";
import {useAuthContext} from "@/contexts/AuthContext"; import {View} from "react-native-ui-lib";
import {useAtomValue} from "jotai"; import {DeviceType} from "expo-device";
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms"; import * as Device from "expo-device";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit"; 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 {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents"; import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls"; import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
import {EventCell} from "@/components/pages/calendar/EventCell"; import {EventCell} from "@/components/pages/calendar/EventCell";
import {isToday} from "date-fns"; import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
import {View} from "react-native-ui-lib";
import {DeviceType} from "expo-device";
import * as Device from "expo-device"
import {useAtomCallback} from 'jotai/utils'
interface EventCalendarProps { interface EventCalendarProps {
calendarHeight: number;
calendarWidth: number; calendarWidth: number;
mode: "week" | "month" | "day" | "3days"; mode: "week" | "month" | "day" | "3days";
onLoad?: () => void; onLoad?: () => void;
@ -36,14 +35,14 @@ const MODE_TO_DAYS = {
'3days': 3, '3days': 3,
'day': 1, 'day': 1,
'month': 1 'month': 1
}; } as const;
const getContainerProps = (selectedDate: Date) => ({ const getContainerProps = (date: Date, customKey: string) => ({
hourWidth: 70, hourWidth: 70,
allowPinchToZoom: true, allowPinchToZoom: true,
useHaptic: true, useHaptic: true,
scrollToNow: true, scrollToNow: true,
initialDate: selectedDate.toISOString(), initialDate: customKey !== "default" ? customKey : date.toISOString(),
}); });
const MemoizedEventCell = React.memo(EventCell, (prev, next) => { const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
@ -51,37 +50,23 @@ const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
prev.event.lastModified === next.event.lastModified; prev.event.lastModified === next.event.lastModified;
}); });
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(( export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
{ calendarWidth,
calendarHeight, mode,
calendarWidth, onLoad
mode, }) => {
onLoad
}) => {
const {profileData} = useAuthContext(); const {profileData} = useAuthContext();
const selectedDate = useAtomValue(selectedDateAtom);
const {data: familyMembers} = useGetFamilyMembers(); const {data: familyMembers} = useGetFamilyMembers();
const calendarRef = useRef<CalendarKitHandle>(null);
const {data: events} = useGetEvents(); const {data: events} = useGetEvents();
const selectedUser = useAtomValue(selectedUserAtom); const selectedUser = useAtomValue(selectedUserAtom);
const calendarRef = useRef<CalendarKitHandle>(null);
const [customKey, setCustomKey] = useState("defaultKey"); const [customKey, setCustomKey] = useState("defaultKey");
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]); 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 {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
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 { const {
handlePressEvent, handlePressEvent,
handlePressCell, handlePressCell,
@ -115,9 +100,12 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
onPressEvent={handlePressEvent} onPressEvent={handlePressEvent}
onPressBackground={handlePressCell} onPressBackground={handlePressCell}
onLoad={onLoad} onLoad={onLoad}
key={customKey}
> >
<CalendarHeader {...HEADER_PROPS} /> <DetailedCalendarController
calendarRef={calendarRef}
setCustomKey={setCustomKey}
/>
<CalendarHeader {...HEADER_PROPS}/>
<CalendarBody <CalendarBody
{...BODY_PROPS} {...BODY_PROPS}
renderEvent={renderEvent} 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 React from 'react';
import { StyleSheet, View, ActivityIndicator } from 'react-native'; import {StyleSheet, View, ActivityIndicator} from 'react-native';
import { Text } from 'react-native-ui-lib'; import {Text} from 'react-native-ui-lib';
import Animated, { import Animated, {
withTiming, withTiming,
useAnimatedStyle, useAnimatedStyle,
FadeOut, FadeOut,
useSharedValue, useSharedValue,
runOnJS
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useGetEvents } from '@/hooks/firebase/useGetEvents'; import {useGetEvents} from '@/hooks/firebase/useGetEvents';
import { useCalSync } from '@/hooks/useCalSync'; import {useCalSync} from '@/hooks/useCalSync';
import { useSyncEvents } from '@/hooks/useSyncOnScroll'; import {useSyncEvents} from '@/hooks/useSyncOnScroll';
import { useAtom } from 'jotai'; import {useAtom} from 'jotai';
import { modeAtom } from './atoms'; import {modeAtom} from './atoms';
import { MonthCalendar } from "@/components/pages/calendar/MonthCalendar"; import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar"; import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
import * as Device from "expo-device"; import * as Device from "expo-device";
export type CalendarMode = 'month' | 'day' | '3days' | 'week'; export type CalendarMode = 'month' | 'day' | '3days' | 'week';
interface EventCalendarProps { interface EventCalendarProps {
calendarHeight: number;
calendarWidth: number; calendarWidth: number;
} }
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => { export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
const { isLoading } = useGetEvents(); const {isLoading} = useGetEvents();
const [mode] = useAtom<CalendarMode>(modeAtom); const [mode] = useAtom<CalendarMode>(modeAtom);
const { isSyncing } = useSyncEvents(); const {isSyncing} = useSyncEvents();
const isTablet = Device.deviceType === Device.DeviceType.TABLET; const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const isCalendarReady = useSharedValue(false); const isCalendarReady = useSharedValue(false);
useCalSync(); useCalSync();
@ -37,26 +35,26 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
}, []); }, []);
const containerStyle = useAnimatedStyle(() => ({ const containerStyle = useAnimatedStyle(() => ({
opacity: withTiming(isCalendarReady.value ? 1 : 0, { duration: 500 }), opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
flex: 1, flex: 1,
})); }));
const monthStyle = useAnimatedStyle(() => ({ const monthStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'month' ? 1 : 0, { duration: 300 }), opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
position: 'absolute', position: 'absolute',
width: '100%', width: '100%',
height: '100%', height: '100%',
})); }));
const detailedDayStyle = useAnimatedStyle(() => ({ const detailedDayStyle = useAnimatedStyle(() => ({
opacity: withTiming(mode === 'day' ? 1 : 0, { duration: 300 }), opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
position: 'absolute', position: 'absolute',
width: '100%', width: '100%',
height: '100%', height: '100%',
})); }));
const detailedMultiStyle = useAnimatedStyle(() => ({ 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', position: 'absolute',
width: '100%', width: '100%',
height: '100%', height: '100%',
@ -64,7 +62,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
return ( return (
<View style={styles.root}> <View style={styles.root}>
{(isLoading || isSyncing) && mode !== 'month' && ( {(isLoading || isSyncing) && mode !== 'month' && (
<Animated.View <Animated.View
exiting={FadeOut.duration(300)} exiting={FadeOut.duration(300)}
style={styles.loadingContainer} style={styles.loadingContainer}
@ -75,14 +73,19 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
)} )}
<Animated.View style={containerStyle}> <Animated.View style={containerStyle}>
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}> <Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
<MonthCalendar {...props} /> <MonthCalendar/>
</Animated.View> </Animated.View>
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}> <Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
<DetailedCalendar mode="day" {...props} /> <DetailedCalendar mode="day" {...props} />
</Animated.View> </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 && ( {!isLoading && (
<DetailedCalendar onLoad={handleRenderComplete} mode={isTablet ? 'week' : '3days'} {...props} /> <DetailedCalendar
onLoad={handleRenderComplete}
mode={isTablet ? 'week' : '3days'}
{...props}
/>
)} )}
</Animated.View> </Animated.View>
</Animated.View> </Animated.View>

View File

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

View File

@ -122,7 +122,7 @@ export const ManuallyAddEventModal = () => {
const { const {
mutateAsync: createEvent, mutateAsync: createEvent,
isLoading: isAdding, isPending: isAdding,
isError, isError,
} = useCreateEvent(); } = useCreateEvent();
const {data: members} = useGetFamilyMembers(true); const {data: members} = useGetFamilyMembers(true);
@ -178,15 +178,6 @@ export const ManuallyAddEventModal = () => {
if (!show) return null; 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 = () => { const showDeleteEventModal = () => {
setDeleteModalVisible(true); setDeleteModalVisible(true);
}; };
@ -257,38 +248,6 @@ export const ManuallyAddEventModal = () => {
return true; 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) { if (isLoading && !isError) {
return ( return (
<Modal <Modal
@ -355,7 +314,7 @@ export const ManuallyAddEventModal = () => {
setDate(newDate); setDate(newDate);
if(isStart) { if(isStart) {
if (startTime.getHours() > endTime.getHours() && if (startTime.getHours() > endTime.getHours() &&
(isSameDay(newDate, endDate) || isAfter(newDate, endDate))) { (isSameDay(newDate, endDate) || isAfter(newDate, endDate))) {
const newEndDate = new Date(newDate); const newEndDate = new Date(newDate);
newEndDate.setDate(newEndDate.getDate() + 1); newEndDate.setDate(newEndDate.getDate() + 1);
@ -364,7 +323,7 @@ export const ManuallyAddEventModal = () => {
setEndDate(newEndDate); setEndDate(newEndDate);
} }
} else { } else {
if (endTime.getHours() < startTime.getHours() && if (endTime.getHours() < startTime.getHours() &&
(isSameDay(newDate, startDate) || isAfter(startDate, newDate))) { (isSameDay(newDate, startDate) || isAfter(startDate, newDate))) {
const newStartDate = new Date(newDate); const newStartDate = new Date(newDate);
newStartDate.setDate(newStartDate.getDate() - 1); newStartDate.setDate(newStartDate.getDate() - 1);
@ -432,7 +391,7 @@ export const ManuallyAddEventModal = () => {
is24Hour={profileData?.userType === ProfileType.PARENT ? false : true} is24Hour={profileData?.userType === ProfileType.PARENT ? false : true}
onChange={(time) => { onChange={(time) => {
if (endDate.getDate() === startDate.getDate() && 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); const newEndDate = new Date(endDate);
@ -801,9 +760,9 @@ export const ManuallyAddEventModal = () => {
<CameraIcon color="white"/> <CameraIcon color="white"/>
</View> </View>
)} )}
/> />
{editEvent && ( {editEvent && (
<TouchableOpacity <TouchableOpacity
onPress={showDeleteEventModal} onPress={showDeleteEventModal}
style={{ marginTop: 15, marginBottom: 40, alignSelf: "center" }} style={{ marginTop: 15, marginBottom: 40, alignSelf: "center" }}
hitSlop={{left: 30, right: 30, top: 10, bottom: 10}} 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 React, {useCallback, useMemo, useRef} from 'react';
import { import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
Dimensions,
StyleSheet,
Text,
TouchableOpacity,
View,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native';
import { import {
addDays, addDays,
addMonths,
eachDayOfInterval, eachDayOfInterval,
endOfMonth, endOfMonth,
format, format,
isSameDay, isSameDay,
isSameMonth, isSameMonth,
isWithinInterval,
startOfMonth, startOfMonth,
addMonths, startOfWeek, isWithinInterval,
} from 'date-fns'; } from 'date-fns';
import {useGetEvents} from "@/hooks/firebase/useGetEvents"; import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms"; import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext"; import {useAuthContext} from "@/contexts/AuthContext";
import {FlashList} from "@shopify/flash-list"; import {FlashList} from "@shopify/flash-list";
import * as Device from "expo-device"; import * as Device from "expo-device";
import debounce from "debounce"; import {CalendarController} from "@/components/pages/calendar/CalendarController";
interface CalendarEvent { interface CalendarEvent {
id: string; id: string;
@ -40,12 +33,12 @@ 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 CENTER_MONTH_INDEX = 24; const CENTER_MONTH_INDEX = 12;
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => ( const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
<TouchableOpacity <TouchableOpacity
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]} style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
onPress={onPress} onPress={onPress}
> >
<Text style={styles.eventText} numberOfLines={1}> <Text style={styles.eventText} numberOfLines={1}>
@ -78,7 +71,7 @@ const MultiDayEvent = React.memo(({
const style = { const style = {
position: 'absolute' as const, position: 'absolute' as const,
height: 14, height: 14,
backgroundColor: event.color || '#6200ee', backgroundColor: event?.eventColor || '#6200ee',
padding: 2, padding: 2,
zIndex: 1, zIndex: 1,
left: isStart ? 4 : -0.5, // Extend slightly into the border left: isStart ? 4 : -0.5, // Extend slightly into the border
@ -103,22 +96,21 @@ const MultiDayEvent = React.memo(({
); );
}); });
const Day = React.memo(({ const Day = React.memo((
date, {
selectedDate, date,
events, events,
multiDayEvents, multiDayEvents,
dayWidth, dayWidth,
onPress onPress
}: { }: {
date: Date; date: Date;
selectedDate: Date; events: CalendarEvent[];
events: CalendarEvent[]; multiDayEvents: CalendarEvent[];
multiDayEvents: CalendarEvent[]; dayWidth: number;
dayWidth: number; onPress: (date: Date) => void;
onPress: (date: Date) => void; }) => {
}) => { const isCurrentMonth = isSameMonth(date, new Date());
const isCurrentMonth = isSameMonth(date, selectedDate);
const isToday = isSameDay(date, new Date()); const isToday = isSameDay(date, new Date());
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length); 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 visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
const totalHiddenEvents = singleDayEvents.length - remainingSlots; const totalHiddenEvents = singleDayEvents.length - remainingSlots;
// Calculate space needed for multi-day events
const maxMultiDayPosition = multiDayEvents.length > 0 const maxMultiDayPosition = multiDayEvents.length > 0
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1 ? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
: 0; : 0;
@ -140,7 +131,7 @@ const Day = React.memo(({
> >
<View style={[ <View style={[
styles.dateContainer, styles.dateContainer,
isToday && styles.todayContainer, isToday && {backgroundColor: events?.[0]?.eventColor},
]}> ]}>
<Text style={[ <Text style={[
styles.dateText, 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> = () => { export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
const {data: rawEvents} = useGetEvents(); const {data: rawEvents} = useGetEvents();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom); const setSelectedDate = useSetAtom(selectedDateAtom);
const setMode = useSetAtom(modeAtom); const setMode = useSetAtom(modeAtom);
const {profileData} = useAuthContext(); const {profileData} = useAuthContext();
@ -204,10 +186,8 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
const isTablet = Device.deviceType === Device.DeviceType.TABLET; const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width; 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 screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
const dayWidth = (screenWidth - 32) / 7; const dayWidth = screenWidth / 7;
const centerMonth = useRef(selectedDate); const centerMonth = useRef(new Date());
const isScrolling = useRef(false);
const lastScrollUpdate = useRef<Date>(new Date());
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1; const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
@ -255,10 +235,10 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
}); });
} }
return months; return months;
}, [getMonthData]); }, [getMonthData, rawEvents]);
const processedEvents = useMemo(() => { const processedEvents = useMemo(() => {
if (!rawEvents?.length || !selectedDate) return { if (!rawEvents?.length) return {
eventMap: new Map(), eventMap: new Map(),
multiDayEvents: [] multiDayEvents: []
}; };
@ -324,7 +304,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
<Month <Month
date={item.date} date={item.date}
days={item.days} days={item.days}
selectedDate={selectedDate}
getEventsForDay={getEventsForDay} getEventsForDay={getEventsForDay}
getMultiDayEventsForDay={getMultiDayEventsForDay} getMultiDayEventsForDay={getMultiDayEventsForDay}
dayWidth={dayWidth} dayWidth={dayWidth}
@ -334,31 +313,12 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
/> />
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]); ), [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 ( return (
<View style={styles.container}> <View style={styles.container}>
<CalendarController
scrollViewRef={scrollViewRef}
centerMonthIndex={CENTER_MONTH_INDEX}
/>
<FlashList <FlashList
ref={scrollViewRef} ref={scrollViewRef}
data={monthsToRender} data={monthsToRender}
@ -368,7 +328,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
pagingEnabled pagingEnabled
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
initialScrollIndex={CENTER_MONTH_INDEX} initialScrollIndex={CENTER_MONTH_INDEX}
onScroll={onScroll}
removeClippedSubviews={true} removeClippedSubviews={true}
estimatedItemSize={screenWidth} estimatedItemSize={screenWidth}
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}} estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
@ -391,7 +350,6 @@ const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
const Month = React.memo(({ const Month = React.memo(({
date, date,
days, days,
selectedDate,
getEventsForDay, getEventsForDay,
getMultiDayEventsForDay, getMultiDayEventsForDay,
dayWidth, dayWidth,
@ -401,7 +359,6 @@ const Month = React.memo(({
}: { }: {
date: Date; date: Date;
days: Date[]; days: Date[];
selectedDate: Date;
getEventsForDay: (date: Date) => CalendarEvent[]; getEventsForDay: (date: Date) => CalendarEvent[];
getMultiDayEventsForDay: (date: Date) => CalendarEvent[]; getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
dayWidth: number; dayWidth: number;
@ -481,7 +438,6 @@ const Month = React.memo(({
<Day <Day
key={`${weekIndex}-${dayIndex}`} key={`${weekIndex}-${dayIndex}`}
date={date} date={date}
selectedDate={selectedDate}
events={getEventsForDay(date)} events={getEventsForDay(date)}
multiDayEvents={multiDayEvents} multiDayEvents={multiDayEvents}
dayWidth={dayWidth} dayWidth={dayWidth}
@ -529,11 +485,11 @@ const styles = StyleSheet.create({
}, },
day: { day: {
height: '14%', height: '14%',
padding: 0, // Remove padding padding: 0,
borderWidth: 0.5, borderWidth: 0.5,
borderColor: '#eee', borderColor: '#eee',
position: 'relative', position: 'relative',
overflow: 'visible', // Allow events to overflow overflow: 'visible',
}, },
container: { container: {
flex: 1, flex: 1,
@ -555,7 +511,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'center' // justifyContent: 'center'
}, },
weekDay: { weekDay: {
alignItems: 'center', alignItems: 'center',
@ -609,6 +565,8 @@ const styles = StyleSheet.create({
weekDayRow: { weekDayRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
paddingHorizontal: 16, paddingHorizontal: 0,
}, },
}); });
export default MonthCalendar;

View File

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

View File

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

View File

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

View File

@ -1780,26 +1780,35 @@ async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
}); });
let token = accessToken; let token = accessToken;
// Construct the Google Calendar event
const googleEvent = { const googleEvent = {
summary: event.title, summary: event.title,
start: { start: {
dateTime: event.allDay ? undefined : event.startDate.toISOString(), dateTime: event.allDay ? undefined : event.startDate.toISOString(),
date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined,
timeZone: 'UTC'
}, },
end: { end: {
dateTime: event.allDay ? undefined : event.endDate.toISOString(), dateTime: event.allDay ? undefined : event.endDate.toISOString(),
date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined,
timeZone: 'UTC'
}, },
visibility: event.private ? 'private' : 'default', visibility: event.private ? 'private' : 'default',
id: event.id status: 'confirmed',
reminders: {
useDefault: true
},
// Add extendedProperties to store our Firestore ID
extendedProperties: {
private: {
firestoreId: event.id
}
}
}; };
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${event.id}`; // For new events, use POST to create
const url = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
let response = await fetch(url, { let response = await fetch(url, {
method: 'PUT', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -1820,7 +1829,7 @@ async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
// Retry with new token // Retry with new token
response = await fetch(url, { response = await fetch(url, {
method: 'PUT', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${refreshedGoogleToken}`, 'Authorization': `Bearer ${refreshedGoogleToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -1834,9 +1843,16 @@ async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`); throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`);
} }
console.log('[GOOGLE_SYNC] Successfully synced event to Google Calendar', { const responseData = await response.json();
eventId: event.id,
creatorId // Store the Google Calendar event ID in Firestore
await db.collection('Events').doc(event.id).update({
googleEventId: responseData.id
});
console.log('[GOOGLE_SYNC] Successfully created event in Google Calendar', {
firestoreId: event.id,
googleEventId: responseData.id
}); });
return true; return true;
@ -1900,282 +1916,353 @@ async function deleteEventFromGoogle(eventId, accessToken, refreshToken, creator
} }
} }
const createEventHash = (event) => {
const str = `${event.startDate?.seconds || ''}-${event.endDate?.seconds || ''}-${
event.title || ''
}-${event.location || ''}-${event.allDay ? 'true' : 'false'}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash.toString(36);
};
// Cloud Function to handle event updates async function fetchEventsFromFirestore(userId, profileData, isFamilyView) {
exports.syncEventToGoogleCalendar = functions.firestore const db = admin.firestore();
.document('Events/{eventId}') const eventsQuery = db.collection("Events");
.onWrite(async (change, context) => { let constraints;
const eventId = context.params.eventId; const familyId = profileData?.familyId;
const afterData = change.after.exists ? change.after.data() : null;
const beforeData = change.before.exists ? change.before.data() : null;
// Skip if this is a Google-originated event if (profileData?.userType === "FAMILY_DEVICE") {
if (afterData?.externalOrigin === 'google' || beforeData?.externalOrigin === 'google') { constraints = [
console.log('[GOOGLE_SYNC] Skipping sync for Google-originated event', { eventId }); eventsQuery.where("familyId", "==", familyId)
return null; ];
} else {
if (isFamilyView) {
constraints = [
eventsQuery.where("familyId", "==", familyId),
eventsQuery.where("creatorId", "==", userId),
eventsQuery.where("attendees", "array-contains", userId)
];
} else {
constraints = [
eventsQuery.where("creatorId", "==", userId),
eventsQuery.where("attendees", "array-contains", userId)
];
}
}
try {
const snapshots = await Promise.all(constraints.map(query => query.get()));
const uniqueEvents = new Map();
const processedHashes = new Set();
const creatorIds = new Set();
snapshots.forEach(snapshot => {
snapshot.docs.forEach(doc => {
const event = doc.data();
const hash = createEventHash(event);
if (!processedHashes.has(hash)) {
processedHashes.add(hash);
creatorIds.add(event.creatorId);
uniqueEvents.set(doc.id, event);
} }
});
try {
// Handle deletion
if (!afterData && beforeData) {
console.log('[GOOGLE_SYNC] Processing event deletion', { eventId });
// Only proceed if this was previously synced with Google
if (!beforeData.email) {
return null;
}
const creatorDoc = await db.collection('Profiles').doc(beforeData.creatorId).get();
const creatorData = creatorDoc.data();
if (!creatorData?.googleAccounts?.[beforeData.email]) {
return null;
}
const accountData = creatorData.googleAccounts[beforeData.email];
await deleteEventFromGoogle(
eventId,
accountData.accessToken,
accountData.refreshToken,
beforeData.creatorId,
beforeData.email
);
return null;
}
// Handle creation or update
if (afterData) {
// Skip if no creator or email is set
if (!afterData.creatorId || !afterData.email) {
return null;
}
const creatorDoc = await db.collection('Profiles').doc(afterData.creatorId).get();
const creatorData = creatorDoc.data();
if (!creatorData?.googleAccounts?.[afterData.email]) {
return null;
}
const accountData = creatorData.googleAccounts[afterData.email];
await syncEventToGoogle(
afterData,
accountData.accessToken,
accountData.refreshToken,
afterData.creatorId
);
}
return null;
} catch (error) {
console.error('[GOOGLE_SYNC] Error in sync function:', error);
// Store the error for later retry or monitoring
await db.collection('SyncErrors').add({
eventId,
error: error.message,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
type: 'google'
});
throw error;
}
}););
let token = accessToken;
// Construct the Google Calendar event
const googleEvent = {
summary: event.title,
start: {
dateTime: event.allDay ? undefined : event.startDate.toISOString(),
date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined
},
end: {
dateTime: event.allDay ? undefined : event.endDate.toISOString(),
date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined
},
visibility: event.private ? 'private' : 'default',
id: event.id
};
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${event.id}`;
let response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(googleEvent)
}); });
// Handle token refresh if needed const creatorIdsArray = Array.from(creatorIds);
if (response.status === 401 && refreshToken) { const creatorProfiles = new Map();
console.log('[GOOGLE_SYNC] Token expired, refreshing...'); const BATCH_SIZE = 10;
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
token = refreshedGoogleToken;
// Update the token in Firestore for (let i = 0; i < creatorIdsArray.length; i += BATCH_SIZE) {
await db.collection("Profiles").doc(creatorId).update({ const chunk = creatorIdsArray.slice(i, i + BATCH_SIZE);
[`googleAccounts.${event.email}.accessToken`]: refreshedGoogleToken const profilesSnapshot = await db
.collection("Profiles")
.where(admin.firestore.FieldPath.documentId(), "in", chunk)
.get();
profilesSnapshot.docs.forEach(doc => {
creatorProfiles.set(doc.id, doc.data()?.eventColor || '#ff69b4');
}); });
}
// Retry with new token return Array.from(uniqueEvents.entries()).map(([id, event]) => ({
response = await fetch(url, { ...event,
method: 'PUT', id,
start: event.allDay
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
: new Date(event.startDate.seconds * 1000),
end: event.allDay
? new Date(new Date(event.endDate.seconds * 1000).setHours(0, 0, 0, 0))
: new Date(event.endDate.seconds * 1000),
hideHours: event.allDay,
eventColor: creatorProfiles.get(event.creatorId) || '#ff69b4',
notes: event.notes
}));
} catch (error) {
console.error('Error fetching events:', error);
throw new functions.https.HttpsError('internal', 'Error fetching events');
}
}
exports.fetchEvents = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'User must be authenticated'
);
}
try {
const { isFamilyView } = data;
const userId = context.auth.uid;
const profileDoc = await admin.firestore()
.collection('Profiles')
.doc(userId)
.get();
if (!profileDoc.exists) {
throw new functions.https.HttpsError(
'not-found',
'User profile not found'
);
}
const profileData = profileDoc.data();
const events = await fetchEventsFromFirestore(userId, profileData, isFamilyView);
return { events };
} catch (error) {
console.error('Error in fetchEvents:', error);
throw new functions.https.HttpsError(
'internal',
error.message || 'An unknown error occurred'
);
}
});
exports.syncNewEventToGoogle = functions.firestore
.document('Events/{eventId}')
.onCreate(async (snapshot, context) => {
const newEvent = snapshot.data();
const eventId = context.params.eventId;
// Don't sync if this event came from Google
if (newEvent.externalOrigin === 'google') {
console.log('[GOOGLE_SYNC] Skipping sync for Google-originated event:', eventId);
return;
}
try {
// Get the creator's Google account credentials
const creatorDoc = await db.collection('Profiles').doc(newEvent.creatorId).get();
const creatorData = creatorDoc.data();
if (!creatorData?.googleAccounts) {
console.log('[GOOGLE_SYNC] Creator has no Google accounts:', newEvent.creatorId);
return;
}
// Get the first Google account (assuming one account per user)
const [email, accountData] = Object.entries(creatorData.googleAccounts)[0];
if (!accountData?.accessToken) {
console.log('[GOOGLE_SYNC] No access token found for creator:', newEvent.creatorId);
return;
}
await syncEventToGoogle(
{
...newEvent,
email,
startDate: new Date(newEvent.startDate.seconds * 1000),
endDate: new Date(newEvent.endDate.seconds * 1000)
},
accountData.accessToken,
accountData.refreshToken,
newEvent.creatorId
);
console.log('[GOOGLE_SYNC] Successfully synced new event to Google:', eventId);
} catch (error) {
console.error('[GOOGLE_SYNC] Error syncing new event to Google:', error);
}
});
exports.syncEventToGoogleOnUpdate = functions.firestore
.document('Events/{eventId}')
.onUpdate(async (change, context) => {
const eventBefore = change.before.data();
const eventAfter = change.after.data();
const eventId = context.params.eventId;
if (eventAfter.externalOrigin === 'google') {
console.log('[GOOGLE_SYNC] Skipping sync for Google-originated event:', eventId);
return;
}
if (JSON.stringify(eventBefore) === JSON.stringify(eventAfter)) {
console.log('[GOOGLE_SYNC] No changes detected for event:', eventId);
return;
}
try {
const creatorDoc = await db.collection('Profiles').doc(eventAfter.creatorId).get();
const creatorData = creatorDoc.data();
if (!creatorData?.googleAccounts) {
console.log('[GOOGLE_SYNC] Creator has no Google accounts:', eventAfter.creatorId);
return;
}
const [email, accountData] = Object.entries(creatorData.googleAccounts)[0];
if (!accountData?.accessToken) {
console.log('[GOOGLE_SYNC] No access token found for creator:', eventAfter.creatorId);
return;
}
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`;
const googleEvent = {
summary: eventAfter.title,
start: {
dateTime: eventAfter.allDay ? undefined : new Date(eventAfter.startDate.seconds * 1000).toISOString(),
date: eventAfter.allDay ? new Date(eventAfter.startDate.seconds * 1000).toISOString().split('T')[0] : undefined,
timeZone: 'UTC'
},
end: {
dateTime: eventAfter.allDay ? undefined : new Date(eventAfter.endDate.seconds * 1000).toISOString(),
date: eventAfter.allDay ? new Date(new Date(eventAfter.endDate.seconds * 1000).getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined,
timeZone: 'UTC'
},
visibility: eventAfter.private ? 'private' : 'default',
status: 'confirmed',
reminders: {
useDefault: true
}
};
let response = await fetch(url, {
method: 'PATCH',
headers: { headers: {
'Authorization': `Bearer ${refreshedGoogleToken}`, 'Authorization': `Bearer ${accountData.accessToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(googleEvent) body: JSON.stringify(googleEvent)
}); });
}
if (!response.ok) { if (response.status === 401 && accountData.refreshToken) {
const errorData = await response.json(); console.log('[GOOGLE_SYNC] Token expired, refreshing...');
throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`); const { refreshedGoogleToken } = await refreshGoogleToken(accountData.refreshToken);
} await db.collection("Profiles").doc(eventAfter.creatorId).update({
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
});
console.log('[GOOGLE_SYNC] Successfully synced event to Google Calendar', { response = await fetch(url, {
eventId: event.id, method: 'PATCH',
creatorId headers: {
}); 'Authorization': `Bearer ${refreshedGoogleToken}`,
'Content-Type': 'application/json'
return true; },
} catch (error) { body: JSON.stringify(googleEvent)
console.error('[GOOGLE_SYNC] Error syncing event to Google Calendar:', error); });
throw error;
}
}
async function deleteEventFromGoogle(eventId, accessToken, refreshToken, creatorId, email) {
try {
console.log('[GOOGLE_DELETE] Starting to delete event from Google Calendar', {
eventId,
creatorId
});
let token = accessToken;
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`;
let response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
} }
});
// Handle token refresh if needed // If event doesn't exist in Google Calendar, create it using insert
if (response.status === 401 && refreshToken) { if (response.status === 404) {
console.log('[GOOGLE_DELETE] Token expired, refreshing...'); console.log('[GOOGLE_SYNC] Event not found in Google Calendar, creating new event');
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken); const insertUrl = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
token = refreshedGoogleToken; response = await fetch(insertUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accountData.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...googleEvent,
id: eventId
})
});
}
// Update the token in Firestore if (!response.ok) {
await db.collection("Profiles").doc(creatorId).update({ const errorData = await response.json();
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken throw new Error(errorData.error?.message || response.statusText);
}); }
// Retry with new token console.log('[GOOGLE_SYNC] Successfully synced event update to Google:', eventId);
response = await fetch(url, { } catch (error) {
method: 'DELETE', console.error('[GOOGLE_SYNC] Error syncing event update to Google:', error);
headers: {
'Authorization': `Bearer ${refreshedGoogleToken}`
}
});
} }
});
if (!response.ok && response.status !== 404) { exports.syncEventToGoogleOnDelete = functions.firestore
const errorData = await response.json();
throw new Error(`Failed to delete event: ${errorData.error?.message || response.statusText}`);
}
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', {
eventId,
creatorId
});
return true;
} catch (error) {
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error);
throw error;
}
}
exports.handleEventDelete = functions.firestore
.document('Events/{eventId}') .document('Events/{eventId}')
.onDelete(async (snapshot, context) => { .onDelete(async (snapshot, context) => {
const deletedEvent = snapshot.data(); const deletedEvent = snapshot.data();
const eventId = context.params.eventId; const eventId = context.params.eventId;
// Skip if this was a Google-originated event to prevent sync loops if (deletedEvent.externalOrigin === 'google') {
if (deletedEvent?.externalOrigin === 'google') { console.log('[GOOGLE_SYNC] Skipping delete sync for Google-originated event:', eventId);
console.log('[GOOGLE_DELETE] Skipping delete sync for Google-originated event', { eventId }); return;
return null;
} }
try { try {
// Only proceed if this was synced with Google (has an email) const creatorDoc = await db.collection('Profiles').doc(deletedEvent.creatorId).get();
if (!deletedEvent?.email) {
console.log('[GOOGLE_DELETE] Event not synced with Google, skipping', { eventId });
return null;
}
const creatorDoc = await admin.firestore()
.collection('Profiles')
.doc(deletedEvent.creatorId)
.get();
if (!creatorDoc.exists) {
console.log('[GOOGLE_DELETE] Creator profile not found', {
eventId,
creatorId: deletedEvent.creatorId
});
return null;
}
const creatorData = creatorDoc.data(); const creatorData = creatorDoc.data();
const googleAccount = creatorData?.googleAccounts?.[deletedEvent.email];
if (!googleAccount) { if (!creatorData?.googleAccounts) {
console.log('[GOOGLE_DELETE] No Google account found for email', { console.log('[GOOGLE_SYNC] Creator has no Google accounts:', deletedEvent.creatorId);
eventId, return;
email: deletedEvent.email
});
return null;
} }
await deleteEventFromGoogle( const [email, accountData] = Object.entries(creatorData.googleAccounts)[0];
eventId,
googleAccount.accessToken,
googleAccount.refreshToken,
deletedEvent.creatorId,
deletedEvent.email
);
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', { if (!accountData?.accessToken) {
eventId, console.log('[GOOGLE_SYNC] No access token found for creator:', deletedEvent.creatorId);
email: deletedEvent.email return;
}
const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`;
let response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${accountData.accessToken}`
}
}); });
} catch (error) { if (response.status === 401 && accountData.refreshToken) {
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error); console.log('[GOOGLE_SYNC] Token expired, refreshing...');
const { refreshedGoogleToken } = await refreshGoogleToken(accountData.refreshToken);
// Store the error for monitoring await db.collection("Profiles").doc(deletedEvent.creatorId).update({
await admin.firestore() [`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
.collection('SyncErrors')
.add({
eventId,
error: error.message,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
type: 'google_delete'
}); });
throw error; response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${refreshedGoogleToken}`
}
});
}
if (!response.ok && response.status !== 404) {
const errorData = await response.json();
throw new Error(errorData.error?.message || response.statusText);
}
console.log('[GOOGLE_SYNC] Successfully deleted event from Google:', eventId);
} catch (error) {
console.error('[GOOGLE_SYNC] Error deleting event from Google:', error);
} }
}); });
exports.sendOverviews = functions.pubsub

4001
ios/Podfile.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -90,7 +90,6 @@
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-native": "0.76.5", "react-native": "0.76.5",
"react-native-big-calendar": "^4.15.1",
"react-native-calendars": "^1.1306.0", "react-native-calendars": "^1.1306.0",
"react-native-element-dropdown": "^2.12.2", "react-native-element-dropdown": "^2.12.2",
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",

View File

@ -0,0 +1,148 @@
diff --git a/node_modules/@howljs/calendar-kit/src/hooks/useSyncedList.tsx b/node_modules/@howljs/calendar-kit/src/hooks/useSyncedList.tsx
index 2c2d340..c27d395 100644
--- a/node_modules/@howljs/calendar-kit/src/hooks/useSyncedList.tsx
+++ b/node_modules/@howljs/calendar-kit/src/hooks/useSyncedList.tsx
@@ -40,12 +40,12 @@ const useSyncedList = ({ id }: { id: ScrollType }) => {
const _onMomentumEnd = () => {
if (
- isTriggerMomentum.current &&
- startDateUnix.current !== visibleDateUnix.current
+ isTriggerMomentum.current &&
+ startDateUnix.current !== visibleDateUnix.current
) {
triggerDateChanged.current = undefined;
onDateChanged?.(
- dateTimeToISOString(parseDateTime(visibleDateUnix.current))
+ dateTimeToISOString(parseDateTime(visibleDateUnix.current))
);
notifyDateChanged(visibleDateUnix.current);
isTriggerMomentum.current = false;
@@ -83,71 +83,70 @@ const useSyncedList = ({ id }: { id: ScrollType }) => {
});
const onVisibleColumnChanged = useCallback(
- (props: {
- index: number;
- column: number;
- columns: number;
- offset: number;
- extraScrollData: Record<string, any>;
- }) => {
- const { index: pageIndex, column, columns, extraScrollData } = props;
- const { visibleColumns, visibleDates } = extraScrollData;
+ (props: {
+ index: number;
+ column: number;
+ columns: number;
+ offset: number;
+ extraScrollData: Record<string, any>;
+ }) => {
+ const { index: pageIndex, column, columns, extraScrollData } = props;
+ const { visibleColumns, visibleDates } = extraScrollData;
- if (scrollType.current === id && visibleColumns && visibleDates) {
- const dayIndex = pageIndex * columns + column;
- const visibleStart = visibleDates[pageIndex * columns];
- const visibleEnd =
- visibleDates[pageIndex * columns + column + visibleColumns];
+ if (scrollType.current === id && visibleColumns && visibleDates) {
+ const dayIndex = pageIndex * columns + column;
+ const visibleStart = visibleDates[pageIndex * columns];
+ const visibleEnd =
+ visibleDates[pageIndex * columns + column + visibleColumns];
- if (visibleStart && visibleEnd) {
- const diffDays = Math.floor(
- (visibleEnd - visibleStart) / MILLISECONDS_IN_DAY
- );
- if (diffDays <= 7) {
- visibleWeeks.value = [visibleStart];
- } else {
- const nextWeekStart = visibleDates[pageIndex * columns + 7];
- if (nextWeekStart) {
- visibleWeeks.value = [visibleStart, nextWeekStart];
+ if (visibleStart && visibleEnd) {
+ const diffDays = Math.floor(
+ (visibleEnd - visibleStart) / MILLISECONDS_IN_DAY
+ );
+ if (diffDays <= 7) {
+ visibleWeeks.value = [visibleStart];
+ } else {
+ const nextWeekStart = visibleDates[pageIndex * columns + 7];
+ if (nextWeekStart) {
+ visibleWeeks.value = [visibleStart, nextWeekStart];
+ }
}
}
- }
-
- const currentDate = visibleDates[dayIndex];
- if (!currentDate) {
- triggerDateChanged.current = undefined;
- return;
- }
- if (visibleDateUnix.current !== currentDate) {
- const dateIsoStr = dateTimeToISOString(parseDateTime(currentDate));
- onChange?.(dateIsoStr);
- if (
- triggerDateChanged.current &&
- triggerDateChanged.current === currentDate
- ) {
+ const currentDate = visibleDates[dayIndex];
+ if (!currentDate) {
triggerDateChanged.current = undefined;
- onDateChanged?.(dateIsoStr);
- notifyDateChanged(currentDate);
+ return;
+ }
+
+ if (visibleDateUnix.current !== currentDate) {
+ const dateIsoStr = dateTimeToISOString(parseDateTime(currentDate));
+ onChange?.(dateIsoStr);
+ if (
+ triggerDateChanged?.current === currentDate
+ ) {
+ triggerDateChanged.current = undefined;
+ onDateChanged?.(dateIsoStr);
+ notifyDateChanged(currentDate);
+ }
+ visibleDateUnix.current = currentDate;
+ runOnUI(() => {
+ visibleDateUnixAnim.value = currentDate;
+ })();
}
- visibleDateUnix.current = currentDate;
- runOnUI(() => {
- visibleDateUnixAnim.value = currentDate;
- })();
}
- }
- },
- [
- scrollType,
- id,
- visibleDateUnix,
- visibleWeeks,
- triggerDateChanged,
- onChange,
- onDateChanged,
- notifyDateChanged,
- visibleDateUnixAnim,
- ]
+ },
+ [
+ scrollType,
+ id,
+ visibleDateUnix,
+ visibleWeeks,
+ triggerDateChanged,
+ onChange,
+ onDateChanged,
+ notifyDateChanged,
+ visibleDateUnixAnim,
+ ]
);
return { onScroll, onVisibleColumnChanged };

View File

@ -1,195 +0,0 @@
diff --git a/node_modules/react-native-big-calendar/build/index.js b/node_modules/react-native-big-calendar/build/index.js
index 848ceba..f326b8e 100644
--- a/node_modules/react-native-big-calendar/build/index.js
+++ b/node_modules/react-native-big-calendar/build/index.js
@@ -9,6 +9,17 @@ var isoWeek = require('dayjs/plugin/isoWeek');
var React = require('react');
var reactNative = require('react-native');
var calendarize = require('calendarize');
+var {
+ startOfDay,
+ endOfDay,
+ startOfWeek,
+ isAfter,
+ isBefore,
+ isSameDay,
+ differenceInDays,
+ add,
+ getTime
+} = require('date-fns');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
@@ -1000,87 +1011,91 @@ function _CalendarBodyForMonthView(_a) {
var start = _a.start, end = _a.end;
return day.isBetween(dayjs__default["default"](start).startOf('day'), dayjs__default["default"](end).endOf('day'), null, '[)');
});
- }
- else {
- /**
- * Better way to sort overlapping events that spans accross multiple days
- * For example, if you want following events
- * Event 1, start = 01/01 12:00, end = 02/01 12:00
- * Event 2, start = 02/01 12:00, end = 03/01 12:00
- * Event 3, start = 03/01 12:00, end = 04/01 12:00
- *
- * When drawing calendar in month view, event 3 should be placed at 3rd index for 03/01, because Event 2 are placed at 2nd index for 02/01 and 03/01
- *
- */
- var min_1 = day.startOf('day'), max_1 = day.endOf('day');
- //filter all events that starts from the current week until the current day, and sort them by reverse starting time
- var filteredEvents_1 = events
- .filter(function (_a) {
- var start = _a.start, end = _a.end;
- return dayjs__default["default"](end).isAfter(day.startOf('week')) && dayjs__default["default"](start).isBefore(max_1);
- })
- .sort(function (a, b) {
- if (dayjs__default["default"](a.start).isSame(b.start, 'day')) {
- var aDuration = dayjs__default["default"].duration(dayjs__default["default"](a.end).diff(dayjs__default["default"](a.start))).days();
- var bDuration = dayjs__default["default"].duration(dayjs__default["default"](b.end).diff(dayjs__default["default"](b.start))).days();
- return aDuration - bDuration;
- }
- return b.start.getTime() - a.start.getTime();
+ } else {
+ // Convert day once and cache all the commonly used dates/times
+ const jsDay = day?.toDate?.() || day;
+ const weekStart = startOfWeek(jsDay);
+ var min_1 = startOfDay(jsDay);
+ var max_1 = endOfDay(jsDay);
+ const max1Time = getTime(max_1);
+ const min1Time = getTime(min_1);
+
+ // Pre-process events with dates and cache timestamps
+ const processedEvents = events.map(event => {
+ const startDay = event.start?.toDate?.() || new Date(event.start);
+ const endDay = event.end?.toDate?.() || new Date(event.end);
+ return {
+ ...event,
+ startDay,
+ endDay,
+ startTime: getTime(startDay),
+ endTime: getTime(endDay),
+ startDayStart: startOfDay(startDay)
+ };
});
- /**
- * find the most relevant min date to filter the events
- * in the example:
- * 1. when rendering for 01/01, min date will be 01/01 (start of day for event 1)
- * 2. when rendering for 02/01, min date will be 01/01 (start of day for event 1)
- * 3. when rendering for 03/01, min date will be 01/01 (start of day for event 1)
- * 4. when rendering for 04/01, min date will be 01/01 (start of day for event 1)
- * 5. when rendering for 05/01, min date will be 05/01 (no event overlaps with 05/01)
- */
- filteredEvents_1.forEach(function (_a) {
- var start = _a.start, end = _a.end;
- if (dayjs__default["default"](end).isAfter(min_1) && dayjs__default["default"](start).isBefore(min_1)) {
- min_1 = dayjs__default["default"](start).startOf('day');
+
+ // Filter events within the weekly range and sort by reverse start time
+ let filteredEvents_1 = processedEvents
+ .filter(({ startTime, endTime }) =>
+ endTime > getTime(weekStart) && startTime < max1Time
+ )
+ .sort((a, b) => {
+ if (isSameDay(a.startDay, b.startDay)) {
+ // Pre-calculate durations since they're used in sorting
+ const aDuration = differenceInDays(a.endDay, a.startDay);
+ const bDuration = differenceInDays(b.endDay, b.startDay);
+ return bDuration - aDuration;
+ }
+ return b.startTime - a.startTime;
+ });
+
+ // Update min_1 to the earliest startDay for overlapping events
+ for (const event of filteredEvents_1) {
+ if (event.endTime > min1Time && event.startTime < min1Time) {
+ min_1 = event.startDayStart;
+ break; // We only need the first one due to the sort order
}
- });
+ }
+
+ // Filter to keep only events that overlap the min to max range, then reverse
+ const min1TimeUpdated = getTime(min_1);
filteredEvents_1 = filteredEvents_1
- .filter(function (_a) {
- var start = _a.start, end = _a.end;
- return dayjs__default["default"](end).endOf('day').isAfter(min_1) && dayjs__default["default"](start).isBefore(max_1);
- })
+ .filter(({ startTime, endDay }) =>
+ getTime(endOfDay(endDay)) > min1TimeUpdated && startTime < max1Time
+ )
.reverse();
- /**
- * We move eligible event to the top
- * For example, when rendering for 03/01, Event 3 should be moved to the top, since there is a gap left by Event 1
- */
- var finalEvents_1 = [];
- var tmpDay_1 = day.startOf('week');
- //re-sort events from the start of week until the calendar cell date
- //optimize sorting of event nodes and make sure that no empty gaps are left on top of calendar cell
- while (!tmpDay_1.isAfter(day)) {
- filteredEvents_1.forEach(function (event) {
- if (dayjs__default["default"](event.end).isBefore(tmpDay_1.startOf('day'))) {
- var eventToMoveUp = filteredEvents_1.find(function (e) {
- return dayjs__default["default"](e.start).startOf('day').isSame(tmpDay_1.startOf('day'));
- });
- if (eventToMoveUp != undefined) {
- //remove eventToMoveUp from finalEvents first
- if (finalEvents_1.indexOf(eventToMoveUp) > -1) {
- finalEvents_1.splice(finalEvents_1.indexOf(eventToMoveUp), 1);
- }
- if (finalEvents_1.indexOf(event) > -1) {
- finalEvents_1.splice(finalEvents_1.indexOf(event), 1, eventToMoveUp);
- }
- else {
+
+ // Move eligible events to the top, preventing duplicate entries
+ const finalEvents_1 = [];
+ const seenEvents = new Set(); // Use Set for faster lookups
+ let tmpDay_1 = weekStart;
+
+ while (!isAfter(tmpDay_1, jsDay)) {
+ const tmpDayTime = getTime(tmpDay_1);
+
+ for (const event of filteredEvents_1) {
+ const eventEndDayTime = getTime(startOfDay(event.endDay));
+
+ if (!seenEvents.has(event)) {
+ if (eventEndDayTime < tmpDayTime) {
+ // Find event starting on tmpDay
+ const eventToMoveUp = filteredEvents_1.find(e =>
+ isSameDay(e.startDayStart, tmpDay_1)
+ );
+ if (eventToMoveUp && !seenEvents.has(eventToMoveUp)) {
finalEvents_1.push(eventToMoveUp);
+ seenEvents.add(eventToMoveUp);
}
+ } else {
+ finalEvents_1.push(event);
+ seenEvents.add(event);
}
}
- else if (finalEvents_1.indexOf(event) == -1) {
- finalEvents_1.push(event);
- }
- });
- tmpDay_1 = tmpDay_1.add(1, 'day');
+ }
+ tmpDay_1 = add(tmpDay_1, { days: 1 });
}
+
+ console.log(finalEvents_1);
return finalEvents_1;
}
}, [events, sortedMonthView]);
@@ -1311,7 +1326,7 @@ function _CalendarHeader(_a) {
!stringHasContent(dayHeaderHighlightColor) &&
u['mt-6'],
] }, date.format('D')))),
- showAllDayEventCell ? (React__namespace.createElement(reactNative.View, { style: [
+ showAllDayEventCell ? (React__namespace.createElement(reactNative.ScrollView, { style: [
u['border-l'],
{ borderColor: theme.palette.gray['200'] },
{ height: cellHeight },

View File

@ -4394,11 +4394,6 @@ cacache@^18.0.2:
tar "^6.1.11" tar "^6.1.11"
unique-filename "^3.0.0" unique-filename "^3.0.0"
calendarize@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/calendarize/-/calendarize-1.1.1.tgz#0fa8b8de6b5e6ff9f9fbb89cc3a8c01aace291c2"
integrity sha512-C2JyBAtNp2NG4DX4fA1EILggLt/5PlYzvQR0crHktoAPBc9TlIfdhzg7tWekCbe+pH6+9qoK+FhPbi+vYJJlqw==
call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
@ -5005,7 +5000,7 @@ date-fns@^3.6.0:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
dayjs@^1.11.13, dayjs@^1.8.15: dayjs@^1.8.15:
version "1.11.13" version "1.11.13"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@ -9618,14 +9613,6 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-native-big-calendar@^4.15.1:
version "4.15.1"
resolved "https://registry.yarnpkg.com/react-native-big-calendar/-/react-native-big-calendar-4.15.1.tgz#9cfef290e40a51cbc59b1be4d065804b21f2f74f"
integrity sha512-hNrzkM+9Kb2T0J/1fW9AMaeN+AuhakCfNtQPaQL29l3JXgOO14ikJ3iPqQkmNVbuiWYiMrpI25hrmXffiOVIgQ==
dependencies:
calendarize "^1.1.1"
dayjs "^1.11.13"
react-native-calendars@^1.1306.0: react-native-calendars@^1.1306.0:
version "1.1307.0" version "1.1307.0"
resolved "https://registry.yarnpkg.com/react-native-calendars/-/react-native-calendars-1.1307.0.tgz#2cb8c5abd322037e9b3ef5605afa86f25347068f" resolved "https://registry.yarnpkg.com/react-native-calendars/-/react-native-calendars-1.1307.0.tgz#2cb8c5abd322037e9b3ef5605afa86f25347068f"