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 { Drawer } from "expo-router/drawer";
|
||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
||||
import {
|
||||
DrawerContentScrollView,
|
||||
DrawerNavigationOptions,
|
||||
DrawerNavigationProp,
|
||||
} from "@react-navigation/drawer";
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import { ImageBackground, Pressable, StyleSheet } from "react-native";
|
||||
import React, {useCallback} from "react";
|
||||
import {Drawer} from "expo-router/drawer";
|
||||
import {DrawerContentScrollView, DrawerContentComponentProps, DrawerNavigationOptions} from "@react-navigation/drawer";
|
||||
import {ImageBackground, Pressable, StyleSheet} from "react-native";
|
||||
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||
import * as Device from "expo-device";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {Ionicons} from "@expo/vector-icons";
|
||||
import {DeviceType} from "expo-device";
|
||||
import {DrawerNavigationProp} from "@react-navigation/drawer";
|
||||
import {ParamListBase, Theme} from '@react-navigation/native';
|
||||
import {RouteProp} from "@react-navigation/native";
|
||||
|
||||
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
|
||||
import DrawerButton from "@/components/shared/DrawerButton";
|
||||
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||
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 DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||
import { RouteProp } from "@react-navigation/core";
|
||||
import RefreshButton from "@/components/shared/RefreshButton";
|
||||
import { useCalSync } from "@/hooks/useCalSync";
|
||||
import {useIsFetching} from "@tanstack/react-query";
|
||||
import { CalendarHeader } from "@/components/pages/calendar/CalendarHeader";
|
||||
import {
|
||||
isFamilyViewAtom,
|
||||
settingsPageIndex,
|
||||
toDosPageIndex,
|
||||
userSettingsView,
|
||||
} from "@/components/pages/calendar/atoms";
|
||||
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
|
||||
|
||||
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||
|
||||
type DrawerParamList = {
|
||||
index: undefined;
|
||||
calendar: undefined;
|
||||
todos: undefined;
|
||||
index: undefined;
|
||||
calendar: 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 {
|
||||
navigation: NavigationProp;
|
||||
interface DrawerButtonConfig {
|
||||
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 {
|
||||
routeName: keyof DrawerParamList;
|
||||
navigation: NavigationProp;
|
||||
route: RouteProp<DrawerParamList>;
|
||||
navigation: DrawerScreenNavigationProp;
|
||||
}
|
||||
|
||||
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({ navigation }) => (
|
||||
<ViewSwitch navigation={navigation} />
|
||||
));
|
||||
const HeaderRight: React.FC<HeaderRightProps> = ({route, navigation}) => {
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
|
||||
const isCalendarPage = ["calendar", "index"].includes(route.name);
|
||||
|
||||
const HeaderRight = React.memo<HeaderRightProps>(
|
||||
({ routeName, navigation }) => {
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(routeName);
|
||||
|
||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||
return null;
|
||||
if (!isTablet || !showViewSwitch) {
|
||||
return isCalendarPage ? <CalendarHeader/> : null;
|
||||
}
|
||||
|
||||
return <MemoizedViewSwitch navigation={navigation} />;
|
||||
}
|
||||
);
|
||||
export default function TabLayout() {
|
||||
const { mutateAsync: signOut } = useSignOut();
|
||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||
const setUserView = useSetAtom(userSettingsView);
|
||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||
const { resyncAllCalendars, isSyncing } = useCalSync();
|
||||
return (
|
||||
<View marginR-16 row centerV>
|
||||
{isTablet && isCalendarPage && (
|
||||
<View flex-1 center>
|
||||
<CalendarHeader/>
|
||||
</View>
|
||||
)}
|
||||
<ViewSwitch navigation={navigation}/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
|
||||
|
||||
const isLoading = React.useMemo(() => {
|
||||
return isSyncing || isFetching;
|
||||
}, [isSyncing, isFetching]);
|
||||
|
||||
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 => ({
|
||||
const screenOptions: (props: {
|
||||
route: RouteProp<ParamListBase, string>;
|
||||
navigation: DrawerNavigationProp<ParamListBase, string>;
|
||||
theme: Theme;
|
||||
}) => DrawerNavigationOptions = ({route, navigation}) => ({
|
||||
lazy: true,
|
||||
headerShown: true,
|
||||
headerTitleAlign:
|
||||
Device.deviceType === DeviceType.TABLET ? "left" : "unaligned",
|
||||
headerTitle: ({ children }) => {
|
||||
headerTitleAlign: "left",
|
||||
headerTitle: ({children}) => {
|
||||
const isCalendarRoute = ["calendar", "index"].includes(route.name);
|
||||
|
||||
if (isCalendarRoute && Device.deviceType !== DeviceType.TABLET) {
|
||||
return <View centerV><CalendarHeader /></View>;
|
||||
}
|
||||
if (isCalendarRoute) return null;
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
<View flexG centerV paddingL-10>
|
||||
<Text style={styles.headerTitle}>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
headerTitleStyle: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
navigation.toggleDrawer()
|
||||
}}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={({ pressed }) => [
|
||||
{
|
||||
marginLeft: 16,
|
||||
opacity: pressed ? 0.4 : 1
|
||||
}
|
||||
]}
|
||||
|
||||
>
|
||||
<DrawerIcon />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => navigation.toggleDrawer()}
|
||||
hitSlop={{top: 10, bottom: 10, left: 10, right: 10}}
|
||||
style={({pressed}) => [styles.drawerTrigger, {opacity: pressed ? 0.4 : 1}]}
|
||||
>
|
||||
<DrawerIcon/>
|
||||
</Pressable>
|
||||
),
|
||||
headerRight: () => {
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(
|
||||
route.name
|
||||
);
|
||||
const isCalendarPage = ["calendar", "index"].includes(route.name);
|
||||
headerRight: () => <HeaderRight
|
||||
route={route as RouteProp<DrawerParamList>}
|
||||
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
|
||||
/>,
|
||||
drawerStyle: styles.drawer,
|
||||
});
|
||||
|
||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||
return isCalendarPage ? (
|
||||
<View marginR-8 >
|
||||
<RefreshButton onRefresh={onRefresh} isSyncing={isLoading} />
|
||||
</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>
|
||||
);
|
||||
interface DrawerScreen {
|
||||
name: keyof DrawerParamList;
|
||||
title: string;
|
||||
hideInDrawer?: boolean;
|
||||
}
|
||||
|
||||
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({
|
||||
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 },
|
||||
label: { fontFamily: "Poppins_400Medium", fontSize: 15 },
|
||||
title: {
|
||||
fontSize: 26.13,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
color: "#262627",
|
||||
},
|
||||
drawer: {
|
||||
width: isTablet ? "30%" : "90%",
|
||||
backgroundColor: "#f9f8f7",
|
||||
height: "100%",
|
||||
},
|
||||
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 "react-native-reanimated";
|
||||
import {AuthContextProvider} from "@/contexts/AuthContext";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
import {TextProps, ThemeManager, Toast, Typography,} from "react-native-ui-lib";
|
||||
import {Platform} from 'react-native';
|
||||
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 auth from "@react-native-firebase/auth";
|
||||
|
||||
enableScreens(true)
|
||||
enableFreeze(true)
|
||||
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
|
@ -20,7 +20,7 @@ const UsersList = () => {
|
||||
const { user: currentUser } = useAuthContext();
|
||||
const { data: familyMembers, refetch: refetchFamilyMembers } =
|
||||
useGetFamilyMembers();
|
||||
const [selectedUser, setSelectedUser] = useAtom<UserProfile | null>(selectedUserAtom);
|
||||
const [selectedUser, setSelectedUser] = useAtom(selectedUserAtom);
|
||||
|
||||
useEffect(() => {
|
||||
refetchFamilyMembers();
|
||||
|
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 {MaterialIcons} from "@expo/vector-icons";
|
||||
import {months} from "./constants";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {useAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {format, isSameDay} from "date-fns";
|
||||
import {format} from "date-fns";
|
||||
import * as Device from "expo-device";
|
||||
import {Mode} from "react-native-big-calendar";
|
||||
import {useIsFetching} from "@tanstack/react-query";
|
||||
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {months} from "./constants";
|
||||
import RefreshButton from "@/components/shared/RefreshButton";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
|
||||
type ViewMode = "day" | "week" | "month" | "3days";
|
||||
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
|
||||
const SEGMENTS = isTablet
|
||||
? [{label: "D"}, {label: "W"}, {label: "M"}]
|
||||
: [{label: "D"}, {label: "3D"}, {label: "M"}];
|
||||
|
||||
const MODE_MAP = {
|
||||
tablet: ["day", "week", "month"],
|
||||
mobile: ["day", "3days", "month"]
|
||||
} as const;
|
||||
|
||||
export const CalendarHeader = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [tempIndex, setTempIndex] = useState<number | null>(null);
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
|
||||
const segments = isTablet
|
||||
? [{label: "D"}, {label: "W"}, {label: "M"}]
|
||||
: [{label: "D"}, {label: "3D"}, {label: "M"}];
|
||||
const {resyncAllCalendars, isSyncing} = useCalSync();
|
||||
const isFetching = useIsFetching({queryKey: ['events']}) > 0;
|
||||
|
||||
const handleSegmentChange = (index: number) => {
|
||||
let selectedMode: Mode;
|
||||
if (isTablet) {
|
||||
selectedMode = ["day", "week", "month"][index] as Mode;
|
||||
} else {
|
||||
selectedMode = ["day", "3days", "month"][index] as Mode;
|
||||
}
|
||||
const isLoading = useMemo(() => isSyncing || isFetching, [isSyncing, isFetching]);
|
||||
|
||||
const handleSegmentChange = useCallback((index: number) => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
const selectedMode = modes[index] as ViewMode;
|
||||
|
||||
setTempIndex(index);
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode);
|
||||
setTempIndex(null);
|
||||
}, 150);
|
||||
}, [setMode]);
|
||||
|
||||
if (selectedMode) {
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode as "day" | "week" | "month" | "3days");
|
||||
setTempIndex(null);
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthChange = (month: string) => {
|
||||
const currentDay = selectedDate.getDate();
|
||||
const currentYear = selectedDate.getFullYear();
|
||||
const handleMonthChange = useCallback((month: string) => {
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
|
||||
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
|
||||
const updatedDate = new Date(
|
||||
selectedDate.getFullYear(),
|
||||
newMonthIndex,
|
||||
selectedDate.getDate()
|
||||
);
|
||||
setSelectedDate(updatedDate);
|
||||
};
|
||||
}, [selectedDate, setSelectedDate]);
|
||||
|
||||
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||
|
||||
const getInitialIndex = () => {
|
||||
if (isTablet) {
|
||||
switch (mode) {
|
||||
case "day":
|
||||
return 0;
|
||||
case "week":
|
||||
return 1;
|
||||
case "month":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
switch (mode) {
|
||||
case "day":
|
||||
return 0;
|
||||
case "3days":
|
||||
return 1;
|
||||
case "month":
|
||||
return 2;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
const handleRefresh = useCallback(async () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
}
|
||||
};
|
||||
}, [resyncAllCalendars]);
|
||||
|
||||
const getInitialIndex = useCallback(() => {
|
||||
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
|
||||
//@ts-ignore
|
||||
return modes.indexOf(mode);
|
||||
}, [mode]);
|
||||
|
||||
const renderMonthPicker = () => (
|
||||
<View row centerV gap-1 flexS>
|
||||
{isTablet && (
|
||||
<Text style={styles.yearText}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
)}
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder="Select Month"
|
||||
style={styles.monthPicker}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={value => handleMonthChange(value as string)}
|
||||
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: styles.yearText,
|
||||
}}
|
||||
>
|
||||
{months.map(month => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: isTablet ? 8 : 0,
|
||||
borderRadius: 20,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
centerV
|
||||
>
|
||||
<View row centerV gap-3>
|
||||
{isTablet && (
|
||||
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
)}
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder={"Select Month"}
|
||||
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
|
||||
}}
|
||||
>
|
||||
{months.map((month) => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
<View style={styles.container} flexG centerV>
|
||||
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
|
||||
|
||||
<View row centerV>
|
||||
<View row centerV flexS>
|
||||
<Button
|
||||
size={"xSmall"}
|
||||
size="xSmall"
|
||||
marginR-1
|
||||
avoidInnerPadding
|
||||
style={styles.todayButton}
|
||||
@ -124,27 +111,52 @@ export const CalendarHeader = memo(() => {
|
||||
<MaterialIcons name="calendar-today" size={30} color="#5f6368"/>
|
||||
<Text style={styles.todayDate}>{format(new Date(), "d")}</Text>
|
||||
</Button>
|
||||
<View>
|
||||
|
||||
<View style={styles.segmentContainer}>
|
||||
<SegmentedControl
|
||||
segments={segments}
|
||||
segments={SEGMENTS}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentslblStyle}
|
||||
segmentLabelStyle={styles.segmentLabel}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={tempIndex ?? getInitialIndex()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
segmentslblStyle: {
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: isTablet ? 8 : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
paddingLeft: 10
|
||||
},
|
||||
yearText: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
},
|
||||
monthPicker: {
|
||||
fontFamily: "Manrope_500Medium",
|
||||
fontSize: 17,
|
||||
width: 85,
|
||||
},
|
||||
segmentContainer: {
|
||||
maxWidth: 120,
|
||||
height: 40,
|
||||
},
|
||||
segmentLabel: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
|
@ -9,11 +9,6 @@ export default function CalendarPage() {
|
||||
paddingH-0
|
||||
paddingT-0
|
||||
>
|
||||
{/*<HeaderTemplate
|
||||
message={"Let's get your week started !"}
|
||||
isWelcome
|
||||
isCalendar={true}
|
||||
/>*/}
|
||||
<InnerCalendar />
|
||||
</View>
|
||||
);
|
||||
|
@ -1,21 +1,20 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {modeAtom, selectedDateAtom, selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import React, {useCallback, useMemo, useRef, useState} from "react";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import {DeviceType} from "expo-device";
|
||||
import * as Device from "expo-device";
|
||||
import {CalendarBody, CalendarContainer, CalendarHeader, CalendarKitHandle} from "@howljs/calendar-kit";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {selectedUserAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useFormattedEvents} from "@/components/pages/calendar/useFormattedEvents";
|
||||
import {useCalendarControls} from "@/components/pages/calendar/useCalendarControls";
|
||||
import {EventCell} from "@/components/pages/calendar/EventCell";
|
||||
import {isToday} from "date-fns";
|
||||
import {View} from "react-native-ui-lib";
|
||||
import {DeviceType} from "expo-device";
|
||||
import * as Device from "expo-device"
|
||||
import {useAtomCallback} from 'jotai/utils'
|
||||
import {DetailedCalendarController} from "@/components/pages/calendar/DetailedCalendarController";
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
calendarWidth: number;
|
||||
mode: "week" | "month" | "day" | "3days";
|
||||
onLoad?: () => void;
|
||||
@ -36,14 +35,14 @@ const MODE_TO_DAYS = {
|
||||
'3days': 3,
|
||||
'day': 1,
|
||||
'month': 1
|
||||
};
|
||||
} as const;
|
||||
|
||||
const getContainerProps = (selectedDate: Date) => ({
|
||||
const getContainerProps = (date: Date, customKey: string) => ({
|
||||
hourWidth: 70,
|
||||
allowPinchToZoom: true,
|
||||
useHaptic: true,
|
||||
scrollToNow: true,
|
||||
initialDate: selectedDate.toISOString(),
|
||||
initialDate: customKey !== "default" ? customKey : date.toISOString(),
|
||||
});
|
||||
|
||||
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
|
||||
@ -51,37 +50,23 @@ const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
|
||||
prev.event.lastModified === next.event.lastModified;
|
||||
});
|
||||
|
||||
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
||||
{
|
||||
calendarHeight,
|
||||
calendarWidth,
|
||||
mode,
|
||||
onLoad
|
||||
}) => {
|
||||
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
|
||||
calendarWidth,
|
||||
mode,
|
||||
onLoad
|
||||
}) => {
|
||||
const {profileData} = useAuthContext();
|
||||
const selectedDate = useAtomValue(selectedDateAtom);
|
||||
const {data: familyMembers} = useGetFamilyMembers();
|
||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||
const {data: events} = useGetEvents();
|
||||
const selectedUser = useAtomValue(selectedUserAtom);
|
||||
const calendarRef = useRef<CalendarKitHandle>(null);
|
||||
const [customKey, setCustomKey] = useState("defaultKey");
|
||||
|
||||
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
|
||||
const containerProps = useMemo(() => getContainerProps(selectedDate), [selectedDate]);
|
||||
const currentDate = useMemo(() => new Date(), []);
|
||||
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
|
||||
|
||||
const checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
|
||||
const currentMode = get(modeAtom);
|
||||
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
|
||||
calendarRef?.current?.goToDate({date: selectedDate});
|
||||
setCustomKey(selectedDate.toISOString())
|
||||
}
|
||||
}, [selectedDate]));
|
||||
|
||||
useEffect(() => {
|
||||
checkModeAndGoToDate();
|
||||
}, [selectedDate, checkModeAndGoToDate]);
|
||||
|
||||
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
|
||||
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
|
||||
const {
|
||||
handlePressEvent,
|
||||
handlePressCell,
|
||||
@ -115,9 +100,12 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
|
||||
onPressEvent={handlePressEvent}
|
||||
onPressBackground={handlePressCell}
|
||||
onLoad={onLoad}
|
||||
key={customKey}
|
||||
>
|
||||
<CalendarHeader {...HEADER_PROPS} />
|
||||
<DetailedCalendarController
|
||||
calendarRef={calendarRef}
|
||||
setCustomKey={setCustomKey}
|
||||
/>
|
||||
<CalendarHeader {...HEADER_PROPS}/>
|
||||
<CalendarBody
|
||||
{...BODY_PROPS}
|
||||
renderEvent={renderEvent}
|
||||
|
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 { StyleSheet, View, ActivityIndicator } from 'react-native';
|
||||
import { Text } from 'react-native-ui-lib';
|
||||
import {StyleSheet, View, ActivityIndicator} from 'react-native';
|
||||
import {Text} from 'react-native-ui-lib';
|
||||
import Animated, {
|
||||
withTiming,
|
||||
useAnimatedStyle,
|
||||
FadeOut,
|
||||
useSharedValue,
|
||||
runOnJS
|
||||
} from 'react-native-reanimated';
|
||||
import { useGetEvents } from '@/hooks/firebase/useGetEvents';
|
||||
import { useCalSync } from '@/hooks/useCalSync';
|
||||
import { useSyncEvents } from '@/hooks/useSyncOnScroll';
|
||||
import { useAtom } from 'jotai';
|
||||
import { modeAtom } from './atoms';
|
||||
import { MonthCalendar } from "@/components/pages/calendar/MonthCalendar";
|
||||
import {useGetEvents} from '@/hooks/firebase/useGetEvents';
|
||||
import {useCalSync} from '@/hooks/useCalSync';
|
||||
import {useSyncEvents} from '@/hooks/useSyncOnScroll';
|
||||
import {useAtom} from 'jotai';
|
||||
import {modeAtom} from './atoms';
|
||||
import {MonthCalendar} from "@/components/pages/calendar/MonthCalendar";
|
||||
import DetailedCalendar from "@/components/pages/calendar/DetailedCalendar";
|
||||
import * as Device from "expo-device";
|
||||
|
||||
export type CalendarMode = 'month' | 'day' | '3days' | 'week';
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
calendarWidth: number;
|
||||
}
|
||||
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) => {
|
||||
const { isLoading } = useGetEvents();
|
||||
const {isLoading} = useGetEvents();
|
||||
const [mode] = useAtom<CalendarMode>(modeAtom);
|
||||
const { isSyncing } = useSyncEvents();
|
||||
const {isSyncing} = useSyncEvents();
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const isCalendarReady = useSharedValue(false);
|
||||
useCalSync();
|
||||
@ -37,26 +35,26 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
}, []);
|
||||
|
||||
const containerStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isCalendarReady.value ? 1 : 0, { duration: 500 }),
|
||||
opacity: withTiming(isCalendarReady.value ? 1 : 0, {duration: 500}),
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const monthStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'month' ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === 'month' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const detailedDayStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === 'day' ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === 'day' ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const detailedMultiStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, { duration: 300 }),
|
||||
opacity: withTiming(mode === (isTablet ? 'week' : '3days') ? 1 : 0, {duration: 300}),
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -64,7 +62,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||
{(isLoading || isSyncing) && mode !== 'month' && (
|
||||
<Animated.View
|
||||
exiting={FadeOut.duration(300)}
|
||||
style={styles.loadingContainer}
|
||||
@ -75,14 +73,19 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo((props) =>
|
||||
)}
|
||||
<Animated.View style={containerStyle}>
|
||||
<Animated.View style={monthStyle} pointerEvents={mode === 'month' ? 'auto' : 'none'}>
|
||||
<MonthCalendar {...props} />
|
||||
<MonthCalendar/>
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedDayStyle} pointerEvents={mode === 'day' ? 'auto' : 'none'}>
|
||||
<DetailedCalendar mode="day" {...props} />
|
||||
</Animated.View>
|
||||
<Animated.View style={detailedMultiStyle} pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
|
||||
<Animated.View style={detailedMultiStyle}
|
||||
pointerEvents={mode === (isTablet ? 'week' : '3days') ? 'auto' : 'none'}>
|
||||
{!isLoading && (
|
||||
<DetailedCalendar onLoad={handleRenderComplete} mode={isTablet ? 'week' : '3days'} {...props} />
|
||||
<DetailedCalendar
|
||||
onLoad={handleRenderComplete}
|
||||
mode={isTablet ? 'week' : '3days'}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
|
@ -4,19 +4,16 @@ import {LayoutChangeEvent} from "react-native";
|
||||
import CalendarViewSwitch from "@/components/pages/calendar/CalendarViewSwitch";
|
||||
import {AddEventDialog} from "@/components/pages/calendar/AddEventDialog";
|
||||
import {ManuallyAddEventModal} from "@/components/pages/calendar/ManuallyAddEventModal";
|
||||
import {CalendarHeader} from "@/components/pages/calendar/CalendarHeader";
|
||||
import {EventCalendar} from "@/components/pages/calendar/EventCalendar";
|
||||
|
||||
export const InnerCalendar = () => {
|
||||
const [calendarHeight, setCalendarHeight] = useState(0);
|
||||
const [calendarWidth, setCalendarWidth] = useState(0);
|
||||
const calendarContainerRef = useRef(null);
|
||||
const hasSetInitialSize = useRef(false);
|
||||
|
||||
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (!hasSetInitialSize.current) {
|
||||
const {height, width} = event.nativeEvent.layout;
|
||||
setCalendarHeight(height);
|
||||
const width = event.nativeEvent.layout.width;
|
||||
setCalendarWidth(width);
|
||||
hasSetInitialSize.current = true;
|
||||
}
|
||||
@ -30,12 +27,9 @@ export const InnerCalendar = () => {
|
||||
onLayout={onLayout}
|
||||
paddingB-0
|
||||
>
|
||||
{calendarHeight > 0 && (
|
||||
<EventCalendar
|
||||
calendarHeight={calendarHeight}
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
)}
|
||||
<EventCalendar
|
||||
calendarWidth={calendarWidth}
|
||||
/>
|
||||
</View>
|
||||
<CalendarViewSwitch/>
|
||||
|
||||
|
@ -122,7 +122,7 @@ export const ManuallyAddEventModal = () => {
|
||||
|
||||
const {
|
||||
mutateAsync: createEvent,
|
||||
isLoading: isAdding,
|
||||
isPending: isAdding,
|
||||
isError,
|
||||
} = useCreateEvent();
|
||||
const {data: members} = useGetFamilyMembers(true);
|
||||
@ -178,15 +178,6 @@ export const ManuallyAddEventModal = () => {
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
const formatDateTime = (date?: Date | string) => {
|
||||
if (!date) return undefined;
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const showDeleteEventModal = () => {
|
||||
setDeleteModalVisible(true);
|
||||
};
|
||||
@ -257,38 +248,6 @@ export const ManuallyAddEventModal = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const getRepeatLabel = () => {
|
||||
const selectedDays = repeatInterval;
|
||||
const allDays = [
|
||||
"sunday",
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
];
|
||||
const workDays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
|
||||
|
||||
const isEveryWorkDay = workDays.every((day) => selectedDays.includes(day));
|
||||
|
||||
const isEveryDay = allDays.every((day) => selectedDays.includes(day));
|
||||
|
||||
if (isEveryDay) {
|
||||
return "Every day";
|
||||
} else if (
|
||||
isEveryWorkDay &&
|
||||
!selectedDays.includes("saturday") &&
|
||||
!selectedDays.includes("sunday")
|
||||
) {
|
||||
return "Every work day";
|
||||
} else {
|
||||
return selectedDays
|
||||
.map((item) => daysOfWeek.find((day) => day.value === item)?.label)
|
||||
.join(", ");
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && !isError) {
|
||||
return (
|
||||
<Modal
|
||||
|
@ -1,30 +1,23 @@
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState, useTransition} from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
} from 'react-native';
|
||||
import React, {useCallback, useMemo, useRef} from 'react';
|
||||
import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
|
||||
import {
|
||||
addDays,
|
||||
addMonths,
|
||||
eachDayOfInterval,
|
||||
endOfMonth,
|
||||
format,
|
||||
isSameDay,
|
||||
isSameMonth,
|
||||
isWithinInterval,
|
||||
startOfMonth,
|
||||
addMonths, startOfWeek, isWithinInterval,
|
||||
} from 'date-fns';
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useAtom, useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
import * as Device from "expo-device";
|
||||
import debounce from "debounce";
|
||||
import {CalendarController} from "@/components/pages/calendar/CalendarController";
|
||||
|
||||
interface CalendarEvent {
|
||||
id: string;
|
||||
@ -40,12 +33,12 @@ interface CustomMonthCalendarProps {
|
||||
|
||||
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const MAX_VISIBLE_EVENTS = 3;
|
||||
const CENTER_MONTH_INDEX = 24;
|
||||
const CENTER_MONTH_INDEX = 12;
|
||||
|
||||
|
||||
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
|
||||
<TouchableOpacity
|
||||
style={[styles.event, {backgroundColor: event.color || '#6200ee'}]}
|
||||
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={styles.eventText} numberOfLines={1}>
|
||||
@ -78,7 +71,7 @@ const MultiDayEvent = React.memo(({
|
||||
const style = {
|
||||
position: 'absolute' as const,
|
||||
height: 14,
|
||||
backgroundColor: event.color || '#6200ee',
|
||||
backgroundColor: event?.eventColor || '#6200ee',
|
||||
padding: 2,
|
||||
zIndex: 1,
|
||||
left: isStart ? 4 : -0.5, // Extend slightly into the border
|
||||
@ -103,22 +96,21 @@ const MultiDayEvent = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
const Day = React.memo(({
|
||||
date,
|
||||
selectedDate,
|
||||
events,
|
||||
multiDayEvents,
|
||||
dayWidth,
|
||||
onPress
|
||||
}: {
|
||||
date: Date;
|
||||
selectedDate: Date;
|
||||
events: CalendarEvent[];
|
||||
multiDayEvents: CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
}) => {
|
||||
const isCurrentMonth = isSameMonth(date, selectedDate);
|
||||
const Day = React.memo((
|
||||
{
|
||||
date,
|
||||
events,
|
||||
multiDayEvents,
|
||||
dayWidth,
|
||||
onPress
|
||||
}: {
|
||||
date: Date;
|
||||
events: CalendarEvent[];
|
||||
multiDayEvents: CalendarEvent[];
|
||||
dayWidth: number;
|
||||
onPress: (date: Date) => void;
|
||||
}) => {
|
||||
const isCurrentMonth = isSameMonth(date, new Date());
|
||||
const isToday = isSameDay(date, new Date());
|
||||
|
||||
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length);
|
||||
@ -126,7 +118,6 @@ const Day = React.memo(({
|
||||
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
|
||||
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
|
||||
|
||||
// Calculate space needed for multi-day events
|
||||
const maxMultiDayPosition = multiDayEvents.length > 0
|
||||
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
|
||||
: 0;
|
||||
@ -140,7 +131,7 @@ const Day = React.memo(({
|
||||
>
|
||||
<View style={[
|
||||
styles.dateContainer,
|
||||
isToday && styles.todayContainer,
|
||||
isToday && {backgroundColor: events?.[0]?.eventColor},
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.dateText,
|
||||
@ -185,18 +176,9 @@ const Day = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const findFirstAvailablePosition = (usedPositions: number[]): number => {
|
||||
let position = 0;
|
||||
while (usedPositions.includes(position)) {
|
||||
position++;
|
||||
}
|
||||
return position;
|
||||
};
|
||||
|
||||
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
const {data: rawEvents} = useGetEvents();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||
const setMode = useSetAtom(modeAtom);
|
||||
const {profileData} = useAuthContext();
|
||||
|
||||
@ -204,10 +186,8 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
|
||||
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
|
||||
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
|
||||
const dayWidth = (screenWidth - 32) / 7;
|
||||
const centerMonth = useRef(selectedDate);
|
||||
const isScrolling = useRef(false);
|
||||
const lastScrollUpdate = useRef<Date>(new Date());
|
||||
const dayWidth = screenWidth / 7;
|
||||
const centerMonth = useRef(new Date());
|
||||
|
||||
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
|
||||
|
||||
@ -255,10 +235,10 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
});
|
||||
}
|
||||
return months;
|
||||
}, [getMonthData]);
|
||||
}, [getMonthData, rawEvents]);
|
||||
|
||||
const processedEvents = useMemo(() => {
|
||||
if (!rawEvents?.length || !selectedDate) return {
|
||||
if (!rawEvents?.length) return {
|
||||
eventMap: new Map(),
|
||||
multiDayEvents: []
|
||||
};
|
||||
@ -324,7 +304,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
<Month
|
||||
date={item.date}
|
||||
days={item.days}
|
||||
selectedDate={selectedDate}
|
||||
getEventsForDay={getEventsForDay}
|
||||
getMultiDayEventsForDay={getMultiDayEventsForDay}
|
||||
dayWidth={dayWidth}
|
||||
@ -334,31 +313,12 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
/>
|
||||
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
|
||||
|
||||
const debouncedSetSelectedDate = useMemo(
|
||||
() => debounce(setSelectedDate, 500),
|
||||
[setSelectedDate]
|
||||
);
|
||||
|
||||
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (isScrolling.current) return;
|
||||
|
||||
const currentMonthIndex = Math.round(event.nativeEvent.contentOffset.x / screenWidth);
|
||||
const currentMonth = monthsToRender[currentMonthIndex];
|
||||
|
||||
if (currentMonth) {
|
||||
setSelectedDate(currentMonth.date);
|
||||
centerMonth.current = currentMonth.date;
|
||||
}
|
||||
}, [screenWidth, setSelectedDate, monthsToRender]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedSetSelectedDate.clear();
|
||||
};
|
||||
}, [debouncedSetSelectedDate]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<CalendarController
|
||||
scrollViewRef={scrollViewRef}
|
||||
centerMonthIndex={CENTER_MONTH_INDEX}
|
||||
/>
|
||||
<FlashList
|
||||
ref={scrollViewRef}
|
||||
data={monthsToRender}
|
||||
@ -368,7 +328,6 @@ export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
|
||||
pagingEnabled
|
||||
showsHorizontalScrollIndicator={false}
|
||||
initialScrollIndex={CENTER_MONTH_INDEX}
|
||||
onScroll={onScroll}
|
||||
removeClippedSubviews={true}
|
||||
estimatedItemSize={screenWidth}
|
||||
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
|
||||
@ -391,7 +350,6 @@ const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
|
||||
const Month = React.memo(({
|
||||
date,
|
||||
days,
|
||||
selectedDate,
|
||||
getEventsForDay,
|
||||
getMultiDayEventsForDay,
|
||||
dayWidth,
|
||||
@ -401,7 +359,6 @@ const Month = React.memo(({
|
||||
}: {
|
||||
date: Date;
|
||||
days: Date[];
|
||||
selectedDate: Date;
|
||||
getEventsForDay: (date: Date) => CalendarEvent[];
|
||||
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
|
||||
dayWidth: number;
|
||||
@ -481,7 +438,6 @@ const Month = React.memo(({
|
||||
<Day
|
||||
key={`${weekIndex}-${dayIndex}`}
|
||||
date={date}
|
||||
selectedDate={selectedDate}
|
||||
events={getEventsForDay(date)}
|
||||
multiDayEvents={multiDayEvents}
|
||||
dayWidth={dayWidth}
|
||||
@ -529,11 +485,11 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
day: {
|
||||
height: '14%',
|
||||
padding: 0, // Remove padding
|
||||
padding: 0,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#eee',
|
||||
position: 'relative',
|
||||
overflow: 'visible', // Allow events to overflow
|
||||
overflow: 'visible',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
@ -555,7 +511,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center'
|
||||
// justifyContent: 'center'
|
||||
},
|
||||
weekDay: {
|
||||
alignItems: 'center',
|
||||
@ -609,6 +565,8 @@ const styles = StyleSheet.create({
|
||||
weekDayRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingHorizontal: 16,
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default MonthCalendar;
|
@ -46,7 +46,7 @@ const createEventHash = (event: FormattedEvent): string => {
|
||||
|
||||
// Precompute time constants
|
||||
const DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
const PERIOD_IN_MS = 5 * DAY_IN_MS;
|
||||
const PERIOD_IN_MS = 180 * DAY_IN_MS;
|
||||
const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Memoize date range calculations
|
||||
|
@ -52,7 +52,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
||||
addChoreDialogProps.selectedTodo ?? defaultTodo
|
||||
);
|
||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
|
||||
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid]
|
||||
addChoreDialogProps?.selectedTodo?.assignees ?? [user?.uid!]
|
||||
);
|
||||
const {width} = Dimensions.get("screen");
|
||||
const [points, setPoints] = useState<number>(todo.points);
|
||||
|
@ -10,11 +10,11 @@ interface RefreshButtonProps {
|
||||
}
|
||||
|
||||
const RefreshButton = ({
|
||||
onRefresh,
|
||||
isSyncing,
|
||||
size = 24,
|
||||
color = "#83807F"
|
||||
}: RefreshButtonProps) => {
|
||||
onRefresh,
|
||||
isSyncing,
|
||||
size = 24,
|
||||
color = "#83807F"
|
||||
}: RefreshButtonProps) => {
|
||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||
const rotationLoop = useRef<Animated.CompositeAnimation | null>(null);
|
||||
|
||||
@ -29,12 +29,12 @@ const RefreshButton = ({
|
||||
const startContinuousRotation = () => {
|
||||
rotateAnim.setValue(0);
|
||||
rotationLoop.current = Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
);
|
||||
rotationLoop.current.start();
|
||||
};
|
||||
@ -56,11 +56,28 @@ const RefreshButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} disabled={isSyncing}>
|
||||
<Animated.View style={{ transform: [{ rotate }] }}>
|
||||
<Feather name="refresh-cw" size={size} color={color} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={handlePress}
|
||||
disabled={isSyncing}
|
||||
style={{
|
||||
width: size * 2,
|
||||
height: size + 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
transform: [{ rotate }],
|
||||
width: size,
|
||||
height: size,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Feather name="refresh-cw" size={size} color={color} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1780,26 +1780,35 @@ async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
|
||||
});
|
||||
|
||||
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
|
||||
date: event.allDay ? event.startDate.toISOString().split('T')[0] : undefined,
|
||||
timeZone: 'UTC'
|
||||
},
|
||||
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
|
||||
date: event.allDay ? new Date(event.endDate.getTime() + 24*60*60*1000).toISOString().split('T')[0] : undefined,
|
||||
timeZone: 'UTC'
|
||||
},
|
||||
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, {
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
@ -1820,7 +1829,7 @@ async function syncEventToGoogle(event, accessToken, refreshToken, creatorId) {
|
||||
|
||||
// Retry with new token
|
||||
response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${refreshedGoogleToken}`,
|
||||
'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}`);
|
||||
}
|
||||
|
||||
console.log('[GOOGLE_SYNC] Successfully synced event to Google Calendar', {
|
||||
eventId: event.id,
|
||||
creatorId
|
||||
const responseData = await response.json();
|
||||
|
||||
// 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;
|
||||
@ -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
|
||||
exports.syncEventToGoogleCalendar = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onWrite(async (change, context) => {
|
||||
const eventId = context.params.eventId;
|
||||
const afterData = change.after.exists ? change.after.data() : null;
|
||||
const beforeData = change.before.exists ? change.before.data() : null;
|
||||
async function fetchEventsFromFirestore(userId, profileData, isFamilyView) {
|
||||
const db = admin.firestore();
|
||||
const eventsQuery = db.collection("Events");
|
||||
let constraints;
|
||||
const familyId = profileData?.familyId;
|
||||
|
||||
// Skip if this is a Google-originated event
|
||||
if (afterData?.externalOrigin === 'google' || beforeData?.externalOrigin === 'google') {
|
||||
console.log('[GOOGLE_SYNC] Skipping sync for Google-originated event', { eventId });
|
||||
return null;
|
||||
if (profileData?.userType === "FAMILY_DEVICE") {
|
||||
constraints = [
|
||||
eventsQuery.where("familyId", "==", familyId)
|
||||
];
|
||||
} 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
|
||||
if (response.status === 401 && refreshToken) {
|
||||
console.log('[GOOGLE_SYNC] Token expired, refreshing...');
|
||||
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||
token = refreshedGoogleToken;
|
||||
const creatorIdsArray = Array.from(creatorIds);
|
||||
const creatorProfiles = new Map();
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
// Update the token in Firestore
|
||||
await db.collection("Profiles").doc(creatorId).update({
|
||||
[`googleAccounts.${event.email}.accessToken`]: refreshedGoogleToken
|
||||
for (let i = 0; i < creatorIdsArray.length; i += BATCH_SIZE) {
|
||||
const chunk = creatorIdsArray.slice(i, i + BATCH_SIZE);
|
||||
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
|
||||
response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
return Array.from(uniqueEvents.entries()).map(([id, event]) => ({
|
||||
...event,
|
||||
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: {
|
||||
'Authorization': `Bearer ${refreshedGoogleToken}`,
|
||||
'Authorization': `Bearer ${accountData.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(googleEvent)
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Failed to sync event: ${errorData.error?.message || response.statusText}`);
|
||||
}
|
||||
if (response.status === 401 && accountData.refreshToken) {
|
||||
console.log('[GOOGLE_SYNC] Token expired, refreshing...');
|
||||
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', {
|
||||
eventId: event.id,
|
||||
creatorId
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
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}`
|
||||
response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${refreshedGoogleToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(googleEvent)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle token refresh if needed
|
||||
if (response.status === 401 && refreshToken) {
|
||||
console.log('[GOOGLE_DELETE] Token expired, refreshing...');
|
||||
const { refreshedGoogleToken } = await refreshGoogleToken(refreshToken);
|
||||
token = refreshedGoogleToken;
|
||||
// If event doesn't exist in Google Calendar, create it using insert
|
||||
if (response.status === 404) {
|
||||
console.log('[GOOGLE_SYNC] Event not found in Google Calendar, creating new event');
|
||||
const insertUrl = 'https://www.googleapis.com/calendar/v3/calendars/primary/events';
|
||||
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
|
||||
await db.collection("Profiles").doc(creatorId).update({
|
||||
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error?.message || response.statusText);
|
||||
}
|
||||
|
||||
// Retry with new token
|
||||
response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${refreshedGoogleToken}`
|
||||
}
|
||||
});
|
||||
console.log('[GOOGLE_SYNC] Successfully synced event update to Google:', eventId);
|
||||
} catch (error) {
|
||||
console.error('[GOOGLE_SYNC] Error syncing event update to Google:', error);
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
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
|
||||
exports.syncEventToGoogleOnDelete = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onDelete(async (snapshot, context) => {
|
||||
const deletedEvent = snapshot.data();
|
||||
const eventId = context.params.eventId;
|
||||
|
||||
// Skip if this was a Google-originated event to prevent sync loops
|
||||
if (deletedEvent?.externalOrigin === 'google') {
|
||||
console.log('[GOOGLE_DELETE] Skipping delete sync for Google-originated event', { eventId });
|
||||
return null;
|
||||
if (deletedEvent.externalOrigin === 'google') {
|
||||
console.log('[GOOGLE_SYNC] Skipping delete sync for Google-originated event:', eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Only proceed if this was synced with Google (has an email)
|
||||
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 creatorDoc = await db.collection('Profiles').doc(deletedEvent.creatorId).get();
|
||||
const creatorData = creatorDoc.data();
|
||||
const googleAccount = creatorData?.googleAccounts?.[deletedEvent.email];
|
||||
|
||||
if (!googleAccount) {
|
||||
console.log('[GOOGLE_DELETE] No Google account found for email', {
|
||||
eventId,
|
||||
email: deletedEvent.email
|
||||
});
|
||||
return null;
|
||||
if (!creatorData?.googleAccounts) {
|
||||
console.log('[GOOGLE_SYNC] Creator has no Google accounts:', deletedEvent.creatorId);
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteEventFromGoogle(
|
||||
eventId,
|
||||
googleAccount.accessToken,
|
||||
googleAccount.refreshToken,
|
||||
deletedEvent.creatorId,
|
||||
deletedEvent.email
|
||||
);
|
||||
const [email, accountData] = Object.entries(creatorData.googleAccounts)[0];
|
||||
|
||||
console.log('[GOOGLE_DELETE] Successfully deleted event from Google Calendar', {
|
||||
eventId,
|
||||
email: deletedEvent.email
|
||||
if (!accountData?.accessToken) {
|
||||
console.log('[GOOGLE_SYNC] No access token found for creator:', deletedEvent.creatorId);
|
||||
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) {
|
||||
console.error('[GOOGLE_DELETE] Error deleting event from Google Calendar:', error);
|
||||
|
||||
// Store the error for monitoring
|
||||
await admin.firestore()
|
||||
.collection('SyncErrors')
|
||||
.add({
|
||||
eventId,
|
||||
error: error.message,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
type: 'google_delete'
|
||||
if (response.status === 401 && accountData.refreshToken) {
|
||||
console.log('[GOOGLE_SYNC] Token expired, refreshing...');
|
||||
const { refreshedGoogleToken } = await refreshGoogleToken(accountData.refreshToken);
|
||||
await db.collection("Profiles").doc(deletedEvent.creatorId).update({
|
||||
[`googleAccounts.${email}.accessToken`]: refreshedGoogleToken
|
||||
});
|
||||
|
||||
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-dom": "18.3.1",
|
||||
"react-native": "0.76.5",
|
||||
"react-native-big-calendar": "^4.15.1",
|
||||
"react-native-calendars": "^1.1306.0",
|
||||
"react-native-element-dropdown": "^2.12.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"
|
||||
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:
|
||||
version "1.0.7"
|
||||
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"
|
||||
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||
|
||||
dayjs@^1.11.13, dayjs@^1.8.15:
|
||||
dayjs@^1.8.15:
|
||||
version "1.11.13"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
|
||||
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"
|
||||
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:
|
||||
version "1.1307.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-calendars/-/react-native-calendars-1.1307.0.tgz#2cb8c5abd322037e9b3ef5605afa86f25347068f"
|
||||
|
Reference in New Issue
Block a user