mirror of
https://github.com/urosran/cally.git
synced 2025-07-09 22:57:16 +00:00
Calendar improvements
This commit is contained in:
12
.idea/material_theme_project_new.xml
generated
Normal file
12
.idea/material_theme_project_new.xml
generated
Normal 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>
|
@ -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;
|
@ -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();
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
27
components/pages/calendar/CalendarController.tsx
Normal file
27
components/pages/calendar/CalendarController.tsx
Normal 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;
|
||||||
|
};
|
@ -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",
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
38
components/pages/calendar/DetailedCalendarController.tsx
Normal file
38
components/pages/calendar/DetailedCalendarController.tsx
Normal 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;
|
||||||
|
};
|
@ -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>
|
||||||
|
@ -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/>
|
||||||
|
|
||||||
|
@ -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}}
|
||||||
|
@ -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;
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
4001
ios/Podfile.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
148
patches/@howljs+calendar-kit+2.2.1.patch
Normal file
148
patches/@howljs+calendar-kit+2.2.1.patch
Normal 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 };
|
@ -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 },
|
|
15
yarn.lock
15
yarn.lock
@ -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"
|
||||||
|
Reference in New Issue
Block a user