26 Commits

Author SHA1 Message Date
7a7b1902a8 adujstments 2025-02-16 01:09:46 +01:00
c184eb3293 adujstments 2025-02-16 01:07:12 +01:00
d9ee1cd921 Merge branch 'dev' 2025-02-15 21:08:30 +01:00
b804f37037 Merge branch 'dev'
# Conflicts:
#	app/_layout.tsx
#	firebase/functions/index.js
2025-02-15 16:44:45 +01:00
718fd562ff cicd adjustment 2025-02-14 19:18:01 +01:00
a855e6d3bf cicd adjustment 2025-02-14 19:16:51 +01:00
e04441bd81 Calendar controls fix 2025-02-14 15:05:42 +01:00
f9a5e76162 Calendar controls fix 2025-02-12 01:11:28 +01:00
479e7c5f56 Merge branch 'dev'
# Conflicts:
#	firebase/functions/index.js
2025-02-12 00:43:48 +01:00
3d15d7bb74 Functions update 2025-02-07 14:12:23 +01:00
300ce82a4d Functions update 2025-02-06 09:44:06 +01:00
5ac4526baf Functions update 2025-02-06 01:03:34 +01:00
5cfec1544a Calendar improvements 2025-02-02 22:28:40 +01:00
0998dc29d0 Month fixes 2025-01-26 17:15:13 +01:00
dc7d59eecc Merge branch 'refs/heads/dev' 2025-01-26 16:07:21 +01:00
14be80c6f0 Fixes 2025-01-24 01:12:18 +01:00
580104d052 Month cal changes 2025-01-23 02:16:07 +01:00
231e99ff8f Merge branch 'dev' 2025-01-19 15:28:00 +01:00
bc77c403f2 Month calendar fix 2025-01-17 00:39:20 +01:00
1d4903b609 Merge branch 'dev'
# Conflicts:
#	components/pages/calendar/MonthCalendar.tsx
2025-01-17 00:26:36 +01:00
60953c34bc Month calendar 2025-01-16 00:22:09 +01:00
adafaf1dfe Merge branch 'dev' 2025-01-15 17:39:07 +01:00
0f07e46a82 Merge branch 'dev' 2024-12-30 21:01:59 +01:00
49be903553 Patch fix 2024-12-30 11:39:48 +03:00
8b6171a819 Merge remote-tracking branch 'origin/main' 2024-12-30 11:37:19 +03:00
28110b88cc Merge branch 'dev' 2024-12-28 00:57:25 +01:00
27 changed files with 7074 additions and 2580 deletions

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

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

View File

@ -1,5 +1,5 @@
<resources>
<string name="app_name">"Cally "</string>
<string name="app_name">\"Cally \"</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
<string name="expo_system_ui_user_interface_style" translatable="false">light</string>

View File

@ -1,433 +1,362 @@
import React from "react";
import { Drawer } from "expo-router/drawer";
import { useSignOut } from "@/hooks/firebase/useSignOut";
import React, {memo, useCallback, useMemo} from "react";
import {Drawer} from "expo-router/drawer";
import {
DrawerContentScrollView,
DrawerNavigationOptions,
DrawerNavigationProp,
DrawerContentComponentProps,
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 {ImageBackground, Pressable, StyleSheet} from "react-native";
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
import * as Device from "expo-device";
import {DeviceType} from "expo-device";
import {useSetAtom} from "jotai";
import {Ionicons} from "@expo/vector-icons";
import {ParamListBase, RouteProp, Theme} 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;
}
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({ navigation }) => (
<ViewSwitch navigation={navigation} />
));
const HeaderRight = React.memo<HeaderRightProps>(
({ routeName, navigation }) => {
const showViewSwitch = ["calendar", "todos", "index"].includes(routeName);
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
return 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();
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 => ({
lazy: true,
headerShown: true,
headerTitleAlign:
Device.deviceType === DeviceType.TABLET ? "left" : "unaligned",
headerTitle: ({ children }) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute && Device.deviceType !== DeviceType.TABLET) {
return <View centerV><CalendarHeader /></View>;
}
return (
<Text
style={{
fontFamily: "Manrope_600SemiBold",
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
}}
>
{children}
</Text>
);
},
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>
),
headerRight: () => {
const showViewSwitch = ["calendar", "todos", "index"].includes(
route.name
);
const isCalendarPage = ["calendar", "index"].includes(route.name);
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>
);
navigation: DrawerScreenNavigationProp;
}
const HeaderRight: React.FC<HeaderRightProps> = memo(({route, navigation}) => {
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
const isCalendarPage = ["calendar", "index"].includes(route.name);
if (!isTablet || !showViewSwitch) {
return isCalendarPage ? <CalendarHeader/> : null;
}
return (
<View marginR-16 row centerV>
{isTablet && isCalendarPage && (
<View flex-1 center>
<CalendarHeader/>
</View>
)}
<ViewSwitch navigation={navigation}/>
</View>
)
});
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 = () => {
const screenOptions = useMemo(() => {
return ({route, navigation}: {
route: RouteProp<ParamListBase, string>;
navigation: DrawerNavigationProp<ParamListBase, string>;
theme: Theme;
}): DrawerNavigationOptions => ({
lazy: true,
headerShown: true,
headerTitleAlign: "left",
headerTitle: ({children}) => {
const isCalendarRoute = ["calendar", "index"].includes(route.name);
if (isCalendarRoute) return null;
return (
<View flexG centerV paddingL-10>
<Text style={styles.headerTitle}>
{children}
</Text>
</View>
);
},
headerLeft: () => (
<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: () => <HeaderRight
route={route as RouteProp<DrawerParamList>}
navigation={navigation as DrawerNavigationProp<DrawerParamList>}
/>,
drawerStyle: styles.drawer,
});
}, []);
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;

View File

@ -50,17 +50,17 @@ 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";
import firestore from '@react-native-firebase/firestore';
import functions from '@react-native-firebase/functions';
enableScreens(true)
enableFreeze(true)
SplashScreen.preventAutoHideAsync();
@ -70,9 +70,9 @@ if (Platform.OS === 'ios') {
}
if (__DEV__) {
functions().useEmulator("localhost", 5001);
firestore().useEmulator("localhost", 5471);
auth().useEmulator("http://localhost:9099");
// functions().useEmulator("localhost", 5001);
// firestore().useEmulator("localhost", 5471);
// auth().useEmulator("http://localhost:9099");
}
type TextStyleBase =

View File

@ -1,30 +1,46 @@
import { SegmentedControl, View } from "react-native-ui-lib";
import React, { memo, useCallback } from "react";
import { StyleSheet } from "react-native";
import { NavigationProp, useNavigationState } from "@react-navigation/native";
import {SegmentedControl, View} from "react-native-ui-lib";
import React, {memo, useCallback, useMemo, useRef, useEffect} from "react";
import {StyleSheet} from "react-native";
import {NavigationProp, useNavigationState} from "@react-navigation/native";
interface ViewSwitchProps {
navigation: NavigationProp<any>;
}
const ViewSwitch = memo(function ViewSwitch({ navigation }: ViewSwitchProps) {
const ViewSwitch = memo(function ViewSwitch({navigation}: ViewSwitchProps) {
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
const isInitialMount = useRef(true);
const navigationPending = useRef(false);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}
}, []);
const handleSegmentChange = useCallback(
(index: number) => {
if (index === currentIndex) return;
navigation.navigate(index === 0 ? "calendar" : "todos");
if (navigationPending.current) return;
navigationPending.current = true;
setTimeout(() => {
navigation.navigate(index === 0 ? "calendar" : "todos");
navigationPending.current = false;
}, 300);
},
[navigation, currentIndex]
[navigation]
);
const segments = useMemo(() => [
{label: "Calendar", segmentLabelStyle: styles.labelStyle},
{label: "To Dos", segmentLabelStyle: styles.labelStyle},
], []);
return (
<View style={styles.container}>
<SegmentedControl
segments={[
{ label: "Calendar", segmentLabelStyle: styles.labelStyle },
{ label: "To Dos", segmentLabelStyle: styles.labelStyle },
]}
segments={segments}
containerStyle={styles.segmentContainer}
style={styles.segment}
backgroundColor="#ebebeb"
@ -45,7 +61,7 @@ const styles = StyleSheet.create({
borderRadius: 30,
backgroundColor: "#ebebeb",
shadowColor: "#000",
shadowOffset: { width: 0, height: 0 },
shadowOffset: {width: 0, height: 0},
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
@ -65,4 +81,4 @@ const styles = StyleSheet.create({
},
});
export default ViewSwitch;
export default ViewSwitch;

View File

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

View File

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

View File

@ -1,117 +1,111 @@
import React, {memo} from "react";
import React, {memo, useCallback, useMemo, useState} 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 isTablet = Device.deviceType === Device.DeviceType.TABLET;
const [tempIndex, setTempIndex] = useState<number | null>(null);
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]);
if (selectedMode) {
setTimeout(() => {
setMode(selectedMode as "day" | "week" | "month" | "3days");
}, 150);
}
};
const handleSegmentChange = useCallback((index: number) => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
const selectedMode = modes[index] as ViewMode;
const handleMonthChange = (month: string) => {
const currentDay = selectedDate.getDate();
const currentYear = selectedDate.getFullYear();
setTempIndex(index);
setTimeout(() => {
setMode(selectedMode);
setTempIndex(null);
}, 150);
}, [setMode]);
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]);
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>
const getInitialIndex = useCallback(() => {
const modes = isTablet ? MODE_MAP.tablet : MODE_MAP.mobile;
//@ts-ignore
return modes.indexOf(mode);
}, [mode]);
const renderMonthPicker = () => (
<>
{isTablet && <View flexG/>}
<View row centerV gap-1 flexS>
{isTablet && (
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
<Text style={styles.yearText}>
{selectedDate.getFullYear()}
</Text>
)}
<Picker
value={months[selectedDate.getMonth()]}
placeholder={"Select Month"}
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
placeholder="Select Month"
style={styles.monthPicker}
mode={PickerModes.SINGLE}
onChange={(itemValue) => handleMonthChange(itemValue as string)}
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
onChange={value => handleMonthChange(value as string)}
trailingAccessory={<MaterialIcons name="keyboard-arrow-down"/>}
topBarProps={{
title: selectedDate.getFullYear().toString(),
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
titleStyle: styles.yearText,
}}
>
{months.map((month) => (
{months.map(month => (
<Picker.Item key={month} label={month} value={month}/>
))}
</Picker>
</View>
</>
);
<View row centerV>
return (
<View style={styles.container} flexG centerV>
{mode !== "month" ? renderMonthPicker() : <View flexG/>}
<View row centerV flexS>
<Button
size={"xSmall"}
size="xSmall"
marginR-1
avoidInnerPadding
style={styles.todayButton}
@ -120,27 +114,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={getInitialIndex()}
initialIndex={tempIndex ?? getInitialIndex()}
/>
</View>
<RefreshButton onRefresh={handleRefresh} isSyncing={isLoading}/>
</View>
</View>
);
});
const styles = StyleSheet.create({
segmentslblStyle: {
container: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: isTablet ? 8 : 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
paddingLeft: 10
},
yearText: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
},
monthPicker: {
fontFamily: "Manrope_500Medium",
fontSize: 17,
width: 85,
},
segmentContainer: {
maxWidth: 120,
height: 40,
},
segmentLabel: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
},

View File

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

View File

@ -1,92 +1,94 @@
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
import React from "react";
import { StyleSheet } from "react-native";
import { useAtom } from "jotai";
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
import React, {useState, useCallback} from "react";
import {StyleSheet} from "react-native";
import {useAtom} from "jotai";
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
const CalendarViewSwitch = () => {
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
const [localState, setLocalState] = useState(isFamilyView);
return (
<View
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
// Android shadow (elevation)
elevation: 6,
}}
centerV
>
<TouchableOpacity
onPress={() => {
setIsFamilyView(true);
}}
>
<View
centerV
centerH
height={40}
paddingH-15
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
Family View
</Text>
</View>
</TouchableOpacity>
const handleViewChange = useCallback((newValue: boolean) => {
setLocalState(newValue);
setTimeout(() => {
setIsFamilyView(newValue);
}, 150);
}, [setIsFamilyView]);
<TouchableOpacity
onPress={() => {
setIsFamilyView(false);
}}
>
return (
<View
centerV
centerH
height={40}
paddingH-15
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
row
spread
style={{
position: "absolute",
bottom: 20,
left: 20,
borderRadius: 30,
backgroundColor: "white",
alignItems: "center",
justifyContent: "center",
shadowColor: "#000",
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 6,
}}
centerV
>
<Text
color={!isFamilyView ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
My View
</Text>
<TouchableOpacity
onPress={() => handleViewChange(true)}
>
<View
centerV
centerH
height={40}
paddingH-15
style={localState ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={localState ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
Family View
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleViewChange(false)}
>
<View
centerV
centerH
height={40}
paddingH-15
style={!localState ? styles.switchBtnActive : styles.switchBtn}
>
<Text
color={!localState ? "white" : "#a1a1a1"}
style={styles.switchTxt}
>
My View
</Text>
</View>
</TouchableOpacity>
</View>
</TouchableOpacity>
</View>
);
);
};
export default CalendarViewSwitch;
const styles = StyleSheet.create({
switchBtnActive: {
backgroundColor: "#a1a1a1",
borderRadius: 50,
},
switchBtn: {
backgroundColor: "white",
borderRadius: 50,
},
switchTxt: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
});
switchBtnActive: {
backgroundColor: "#a1a1a1",
borderRadius: 50,
},
switchBtn: {
backgroundColor: "white",
borderRadius: 50,
},
switchTxt: {
fontSize: 16,
fontFamily: "Manrope_600SemiBold",
},
});

View File

@ -1,89 +1,81 @@
import React, {useCallback, useEffect, useMemo, useRef} 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;
}
const MemoizedEventCell = React.memo(EventCell);
const HEADER_PROPS = {
dayBarHeight: 60,
headerBottomHeight: 20,
};
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
{
calendarHeight,
calendarWidth,
mode,
onLoad
}) => {
const BODY_PROPS = {
showNowIndicator: true,
hourFormat: "h:mm a"
};
const MODE_TO_DAYS = {
'week': 7,
'3days': 3,
'day': 1,
'month': 1
} as const;
const getContainerProps = (date: Date, customKey: string) => ({
hourWidth: 70,
allowPinchToZoom: true,
useHaptic: true,
scrollToNow: true,
initialDate: customKey !== "default" ? customKey : date.toISOString(),
});
const MemoizedEventCell = React.memo(EventCell, (prev, next) => {
return prev.event.id === next.event.id &&
prev.event.lastModified === next.event.lastModified;
});
export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo(({
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 checkModeAndGoToDate = useAtomCallback(useCallback((get) => {
const currentMode = get(modeAtom);
if ((selectedDate && isToday(selectedDate)) || currentMode === "month") {
calendarRef?.current?.goToDate({date: selectedDate});
}
}, [selectedDate]));
const memoizedFamilyMembers = useMemo(() => familyMembers || [], [familyMembers]);
const currentDate = useMemo(() => new Date(), []);
const containerProps = useMemo(() => getContainerProps(currentDate, customKey), [currentDate, customKey]);
useEffect(() => {
checkModeAndGoToDate();
}, [selectedDate, checkModeAndGoToDate]);
const {data: formattedEvents} = useFormattedEvents(events ?? [], selectedDate, selectedUser);
const {data: formattedEvents} = useFormattedEvents(events ?? [], currentDate, selectedUser);
const {
handlePressEvent,
handlePressCell,
debouncedOnDateChanged
} = useCalendarControls(events ?? []);
const numberOfDays = useMemo(() => {
return mode === 'week' ? 7 : mode === '3days' ? 3 : 1;
}, [mode]);
const firstDay = useMemo(() => {
return profileData?.firstDayOfWeek === "Mondays" ? 1 : 0;
}, [profileData?.firstDayOfWeek]);
const headerProps = useMemo(() => ({
dayBarHeight: 60,
headerBottomHeight: 20,
}), []);
const bodyProps = useMemo(() => ({
showNowIndicator: true,
hourFormat: "h:mm a"
}), []);
const containerProps = useMemo(() => ({
hourWidth: 70,
allowPinchToZoom: true,
useHaptic: true,
scrollToNow: true,
initialDate: selectedDate.toISOString(),
}), [selectedDate]);
const getAttendees = useCallback((event: any) => {
return familyMembers?.filter(member => event?.attendees?.includes(member?.uid!)) || [];
}, [familyMembers]);
return memoizedFamilyMembers.filter(member => event?.attendees?.includes(member?.uid!));
}, [memoizedFamilyMembers]);
const renderEvent = useCallback((event: any) => {
const attendees = getAttendees(event);
@ -94,27 +86,33 @@ export const DetailedCalendar: React.FC<EventCalendarProps> = React.memo((
attendees={attendees}
/>
);
}, [familyMembers, handlePressEvent, getAttendees]);
}, [getAttendees, handlePressEvent]);
return (
<CalendarContainer
ref={calendarRef}
{...containerProps}
numberOfDays={numberOfDays}
numberOfDays={MODE_TO_DAYS[mode]}
calendarWidth={calendarWidth}
onDateChanged={debouncedOnDateChanged}
firstDay={firstDay}
firstDay={profileData?.firstDayOfWeek === "Mondays" ? 1 : 0}
events={formattedEvents ?? []}
onPressEvent={handlePressEvent}
onPressBackground={handlePressCell}
onLoad={onLoad}
>
<CalendarHeader {...headerProps} />
<DetailedCalendarController
calendarRef={calendarRef}
setCustomKey={setCustomKey}
/>
<CalendarHeader {...HEADER_PROPS}/>
<CalendarBody
{...bodyProps}
{...BODY_PROPS}
renderEvent={renderEvent}
/>
{Device.deviceType === DeviceType.TABLET && <View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>}
{Device.deviceType === DeviceType.TABLET && (
<View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>
)}
</CalendarContainer>
);
});

View File

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

View File

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

View File

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

View File

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

View File

@ -1,336 +1,572 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import {Calendar} from "react-native-big-calendar";
import {ActivityIndicator, StyleSheet, TouchableOpacity, View, ViewStyle} from "react-native";
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useAtom, useSetAtom} from "jotai";
import React, {useCallback, useMemo, useRef} from 'react';
import {Dimensions, StyleSheet, Text, TouchableOpacity, View,} from 'react-native';
import {
editVisibleAtom,
eventForEditAtom,
isAllDayAtom,
isFamilyViewAtom,
modeAtom,
selectedDateAtom,
selectedNewEventDateAtom,
selectedUserAtom,
} from "@/components/pages/calendar/atoms";
addDays,
addMonths,
eachDayOfInterval,
endOfMonth,
format,
isSameDay,
isSameMonth,
isWithinInterval,
startOfMonth,
} from 'date-fns';
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
import {useSetAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {useAuthContext} from "@/contexts/AuthContext";
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
import {Text} from "react-native-ui-lib";
import {addDays, compareAsc, format, isWithinInterval, subDays} from "date-fns";
import {useCalSync} from "@/hooks/useCalSync";
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
import {colorMap, getEventTextColor} from "@/constants/colorMap";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import CachedImage from "expo-cached-image";
import { DeviceType } from "expo-device";
import * as Device from "expo-device"
import {FlashList} from "@shopify/flash-list";
import * as Device from "expo-device";
import {CalendarController} from "@/components/pages/calendar/CalendarController";
interface EventCalendarProps {
calendarHeight: number;
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
calendarWidth: number;
interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
color?: string;
}
const getTotalMinutes = () => {
const date = new Date();
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
};
interface CustomMonthCalendarProps {
weekStartsOn?: 0 | 1;
}
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MAX_VISIBLE_EVENTS = 3;
const CENTER_MONTH_INDEX = 12;
export const MonthCalendar: React.FC<EventCalendarProps> = React.memo(
({calendarHeight}) => {
const {data: events, isLoading} = useGetEvents();
const {profileData, user} = useAuthContext();
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
const [mode, setMode] = useAtom(modeAtom);
const [isFamilyView] = useAtom(isFamilyViewAtom);
const Event = React.memo(({event, onPress}: { event: CalendarEvent; onPress: () => void }) => (
<TouchableOpacity
style={[styles.event, {backgroundColor: event?.eventColor || '#6200ee'}]}
onPress={onPress}
>
<Text style={styles.eventText} numberOfLines={1}>
{event.title}
</Text>
</TouchableOpacity>
));
//tablet view filter
const [selectedUser] = useAtom(selectedUserAtom);
interface CalendarEvent {
id: string;
title: string;
start: Date;
end: Date;
color?: string;
weekPosition?: number;
}
const setEditVisible = useSetAtom(editVisibleAtom);
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
const setEventForEdit = useSetAtom(eventForEditAtom);
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
const MultiDayEvent = React.memo(({
event,
isStart,
isEnd,
onPress,
}: {
event: CalendarEvent;
dayWidth: number;
isStart: boolean;
isEnd: boolean;
onPress: () => void;
}) => {
const style = {
position: 'absolute' as const,
height: 14,
backgroundColor: event?.eventColor || '#6200ee',
padding: 2,
zIndex: 1,
left: isStart ? 4 : -0.5, // Extend slightly into the border
right: isEnd ? 4 : -0.5, // Extend slightly into the border
top: event.weekPosition ? event.weekPosition * 24 : 0,
borderRadius: 4,
borderTopLeftRadius: isStart ? 4 : 0,
borderBottomLeftRadius: isStart ? 4 : 0,
borderTopRightRadius: isEnd ? 4 : 0,
borderBottomRightRadius: isEnd ? 4 : 0,
justifyContent: 'center',
};
const {isSyncing} = useSyncEvents()
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
useCalSync()
return (
<TouchableOpacity style={style} onPress={onPress}>
{isStart && (
<Text style={[styles.eventText]} numberOfLines={1}>
{event.title}
</Text>
)}
</TouchableOpacity>
);
});
const todaysDate = new Date();
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 handlePressEvent = useCallback(
(event: CalendarEvent) => {
if (mode === "day" || mode === "week" || mode === "3days") {
setEditVisible(true);
setEventForEdit(event);
} else {
setMode("day");
setSelectedDate(event.start);
}
},
[setEditVisible, setEventForEdit, mode]
);
const remainingSlots = Math.max(0, MAX_VISIBLE_EVENTS - multiDayEvents.length);
const singleDayEvents = events.filter(event => !event.isMultiDay);
const visibleSingleDayEvents = singleDayEvents.slice(0, remainingSlots);
const totalHiddenEvents = singleDayEvents.length - remainingSlots;
const handlePressCell = useCallback(
(date: Date) => {
date && setSelectedDate(date);
setTimeout(() => {
setMode("day");
}, 100)
},
[mode, setSelectedNewEndDate, setSelectedDate]
);
const maxMultiDayPosition = multiDayEvents.length > 0
? Math.max(...multiDayEvents.map(e => e.weekPosition || 0)) + 1
: 0;
const multiDayEventsHeight = maxMultiDayPosition * 16; // Height for multi-day events
const handlePressDayHeader = useCallback(
(date: Date) => {
if (mode === "day") {
setIsAllDay(true);
setSelectedNewEndDate(date);
setEditVisible(true);
}
if (mode === 'week' || mode === '3days') {
setSelectedDate(date)
setMode("day")
}
},
[mode, setSelectedNewEndDate]
);
const handleSwipeEnd = useCallback(
(date: Date) => {
setSelectedDate(date);
},
[setSelectedDate]
);
const memoizedEventCellStyle = useCallback(
(event: CalendarEvent) => {
let eventColor = event.eventColor ?? colorMap.teal;
let textColor = getEventTextColor(eventColor);
if (!isFamilyView && (event.attendees?.includes(user?.uid!) || event.creatorId! === user?.uid)) {
eventColor = profileData?.eventColor ?? colorMap.teal;
textColor = getEventTextColor(eventColor);
}
return {
backgroundColor: eventColor,
fontSize: 14,
textColor: textColor, // Try adding explicit textColor
style: {
color: textColor // And nested style
}
}
},
[]
);
const memoizedWeekStartsOn = useMemo(
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
[profileData]
);
const isSameDate = useCallback((date1: Date, date2: Date) => {
return (
date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear()
);
}, []);
const dayHeaderColor = useMemo(() => {
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
}, [selectedDate, mode]);
const dateStyle = useMemo(() => {
if (mode === "week" || mode === "3days") return undefined;
return isSameDate(todaysDate, selectedDate) && mode === "day"
? styles.dayHeader
: styles.otherDayHeader;
}, [selectedDate, mode]);
const memoizedHeaderContentStyle = useMemo(() => {
if (mode === "day") {
return styles.dayModeHeader;
} else if (mode === "week" || mode === "3days") {
return styles.weekModeHeader;
} else if (mode === "month") {
return styles.monthModeHeader;
} else {
return {};
}
}, [mode]);
const {filteredEvents} = useMemo(() => {
const startOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
const endOffset = mode === "month" ? 40 : (mode === "week" || mode === "3days") ? 10 : 1;
let eventsToFilter = events ?? [];
if (selectedUser && Device.deviceType === DeviceType.TABLET) {
eventsToFilter = events?.filter(event =>
event.attendees?.includes(selectedUser.uid) ||
event.creatorId === selectedUser.uid
);
}
const filteredEvents =
eventsToFilter?.filter(
(event) =>
event?.start instanceof Date &&
event?.end instanceof Date &&
isWithinInterval(event.start, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
}) &&
isWithinInterval(event.end, {
start: subDays(selectedDate, startOffset),
end: addDays(selectedDate, endOffset),
})
) ?? [];
return {filteredEvents};
}, [events, selectedDate, mode, selectedUser]);
useEffect(() => {
setOffsetMinutes(getTotalMinutes());
}, [events, mode]);
if (isLoading || !events) {
return (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
return (
<View style={[styles.day, {width: dayWidth}]}>
<TouchableOpacity
style={styles.dayContent}
onPress={() => onPress(date)}
>
<View style={[
styles.dateContainer,
isToday && {backgroundColor: events?.[0]?.eventColor},
]}>
<Text style={[
styles.dateText,
!isCurrentMonth && styles.outsideMonthText,
isToday && styles.todayText,
]}>
{format(date, 'd')}
</Text>
</View>
);
{/* Multi-day events container */}
<View style={[styles.multiDayContainer, {height: multiDayEventsHeight}]}>
{multiDayEvents.map(event => (
<MultiDayEvent
key={event.id}
event={event}
dayWidth={dayWidth}
isStart={isSameDay(date, event.start)}
isEnd={isSameDay(date, event.end)}
onPress={() => onPress(event.start)}
/>
))}
</View>
{/* Single-day events container */}
<View style={[styles.singleDayContainer, {marginTop: multiDayEventsHeight}]}>
{visibleSingleDayEvents.map(event => (
<Event
key={event.id}
event={event}
onPress={() => onPress(event.start)}
/>
))}
{totalHiddenEvents > 0 && (
<Text style={styles.moreEvents}>
{totalHiddenEvents} More
</Text>
)}
</View>
</TouchableOpacity>
</View>
);
});
export const MonthCalendar: React.FC<CustomMonthCalendarProps> = () => {
const {data: rawEvents} = useGetEvents();
const setSelectedDate = useSetAtom(selectedDateAtom);
const setMode = useSetAtom(modeAtom);
const {profileData} = useAuthContext();
const scrollViewRef = useRef<FlashList<any>>(null);
const isTablet = Device.deviceType === Device.DeviceType.TABLET;
const screenWidth = isTablet ? Dimensions.get('window').width * 0.89 : Dimensions.get('window').width;
const screenHeight = isTablet ? Dimensions.get('window').height * 0.89 : Dimensions.get('window').height;
const dayWidth = screenWidth / 7;
const centerMonth = useRef(new Date());
const weekStartsOn = profileData?.firstDayOfWeek === "Sundays" ? 0 : 1;
const onDayPress = useCallback(
(date: Date) => {
date && setSelectedDate(date);
setTimeout(() => {
setMode("day");
}, 100)
},
[setSelectedDate, setMode]
);
const getMonthData = useCallback((date: Date) => {
const start = startOfMonth(date);
const end = endOfMonth(date);
const days = eachDayOfInterval({start, end});
const firstDay = days[0];
const startPadding = [];
let startDay = firstDay.getDay();
while (startDay !== weekStartsOn) {
startDay = (startDay - 1 + 7) % 7;
startPadding.unshift(addDays(firstDay, -startPadding.length - 1));
}
return (
<>
{isSyncing && (
<View style={styles.loadingContainer}>
{isSyncing && <Text>Syncing...</Text>}
<ActivityIndicator size="large" color="#0000ff"/>
</View>
)}
<Calendar
bodyContainerStyle={styles.calHeader}
swipeEnabled
mode={"month"}
sortedMonthView
events={filteredEvents}
// renderEvent={renderEvent}
eventCellStyle={memoizedEventCellStyle}
allDayEventCellStyle={memoizedEventCellStyle}
// eventCellTextColor={'#919191'}
//allDayEventCellTextColor={'#919191'}
// enableEnrichedEvents={true}
// enrichedEventsByDate={enrichedEvents}
onPressEvent={handlePressEvent}
weekStartsOn={memoizedWeekStartsOn}
height={calendarHeight}
activeDate={todaysDate}
date={selectedDate}
onPressCell={handlePressCell}
headerContentStyle={memoizedHeaderContentStyle}
onSwipeEnd={handleSwipeEnd}
scrollOffsetMinutes={offsetMinutes}
theme={{
palette: {
nowIndicator: profileData?.eventColor || "#fd1575",
gray: {
"100": "#e8eaed",
"200": "#e8eaed",
"500": "#b7b7b7",
"800": "#919191",
},
},
typography: {
fontFamily: "PlusJakartaSans_500Medium",
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 8},
xl: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 14,
},
moreLabel: {},
xs: {fontSize: 10},
},
}}
dayHeaderStyle={dateStyle}
dayHeaderHighlightColor={"white"}
showAdjacentMonths
headerContainerStyle={mode !== "month" ? {
overflow: "hidden",
} : {}}
hourStyle={styles.hourStyle}
onPressDateHeader={handlePressDayHeader}
ampm
// renderCustomDateForMonth={renderCustomDateForMonth}
/>
{Device.deviceType === DeviceType.TABLET && <View style={{backgroundColor: 'white', height: '9%', width: '100%'}}/>}
</>
const lastDay = days[days.length - 1];
const endPadding = [];
let endDay = lastDay.getDay();
while (endDay !== (weekStartsOn + 6) % 7) {
endDay = (endDay + 1) % 7;
endPadding.push(addDays(lastDay, endPadding.length + 1));
}
);
}
);
return [...startPadding, ...days, ...endPadding];
}, [weekStartsOn]);
const monthsToRender = useMemo(() => {
const months = [];
for (let i = -CENTER_MONTH_INDEX; i <= CENTER_MONTH_INDEX; i++) {
const monthDate = addMonths(centerMonth.current, i);
months.push({
date: monthDate,
days: getMonthData(monthDate)
});
}
return months;
}, [getMonthData, rawEvents]);
const processedEvents = useMemo(() => {
if (!rawEvents?.length) return {
eventMap: new Map(),
multiDayEvents: []
};
const eventMap = new Map();
const multiDayEvents: CalendarEvent[] = [];
rawEvents.forEach((event) => {
if (!event?.start || !event?.end) return;
const startDate = event.start instanceof Date ? event.start : new Date(event.start);
const endDate = event.end instanceof Date ? event.end : new Date(event.end);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) return;
const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
if (duration > 1) {
multiDayEvents.push({
...event,
isMultiDay: true,
start: startDate,
end: endDate
});
} else {
const dateStr = format(startDate, 'yyyy-MM-dd');
const existing = eventMap.get(dateStr) || [];
eventMap.set(dateStr, [...existing, {...event, start: startDate, end: endDate}]);
}
});
multiDayEvents.sort((a, b) => {
if (!a.start || !b.start || !a.end || !b.end) return 0;
const durationA = a.end.getTime() - a.start.getTime();
const durationB = b.end.getTime() - b.start.getTime();
return durationB - durationA;
});
return {eventMap, multiDayEvents};
}, [rawEvents]);
const getMultiDayEventsForDay = useCallback((date: Date) => {
return processedEvents.multiDayEvents.filter(event => {
if (!event.start || !event.end) return false;
return isWithinInterval(date, {
start: event.start,
end: event.end
});
});
}, [processedEvents.multiDayEvents]);
const getEventsForDay = useCallback((date: Date) => {
const dateStr = format(date, 'yyyy-MM-dd');
return processedEvents.eventMap.get(dateStr) || [];
}, [processedEvents.eventMap]);
const sortedDaysOfWeek = useMemo(() => {
const days = [...DAYS_OF_WEEK];
return days.slice(weekStartsOn).concat(days.slice(0, weekStartsOn));
}, [weekStartsOn]);
const renderMonth = useCallback(({item}: { item: MonthData }) => (
<Month
date={item.date}
days={item.days}
getEventsForDay={getEventsForDay}
getMultiDayEventsForDay={getMultiDayEventsForDay}
dayWidth={dayWidth}
onPress={onDayPress}
screenWidth={screenWidth}
sortedDaysOfWeek={sortedDaysOfWeek}
/>
), [getEventsForDay, getMultiDayEventsForDay, dayWidth, onDayPress, screenWidth, weekStartsOn]);
return (
<View style={styles.container}>
<CalendarController
scrollViewRef={scrollViewRef}
centerMonthIndex={CENTER_MONTH_INDEX}
/>
<FlashList
ref={scrollViewRef}
data={monthsToRender}
renderItem={renderMonth}
keyExtractor={keyExtractor}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
initialScrollIndex={CENTER_MONTH_INDEX}
removeClippedSubviews={true}
estimatedItemSize={screenWidth}
estimatedListSize={{width: screenWidth, height: screenHeight * 0.9}}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 10,
}}
/>
</View>
);
};
type MonthData = {
date: Date;
days: Date[];
};
const keyExtractor = (item: MonthData, index: number) => `month-${index}`;
const Month = React.memo(({
date,
days,
getEventsForDay,
getMultiDayEventsForDay,
dayWidth,
onPress,
screenWidth,
sortedDaysOfWeek
}: {
date: Date;
days: Date[];
getEventsForDay: (date: Date) => CalendarEvent[];
getMultiDayEventsForDay: (date: Date) => CalendarEvent[];
dayWidth: number;
onPress: (date: Date) => void;
screenWidth: number;
sortedDaysOfWeek: string[];
}) => {
const weeks = useMemo(() => {
const result = [];
for (let i = 0; i < days.length; i += 7) {
result.push(days.slice(i, i + 7));
}
return result;
}, [days]);
const eventPositions = useMemo(() => {
const positions = new Map<string, number>();
const weekTracking = new Map<number, Set<string>>();
weeks.forEach((week, weekIndex) => {
const activeEvents = new Set<string>();
week.forEach(day => {
const events = getMultiDayEventsForDay(day);
events.forEach(event => {
activeEvents.add(event.id);
});
});
weekTracking.set(weekIndex, activeEvents);
activeEvents.forEach(eventId => {
if (!positions.has(eventId)) {
const prevWeekEvents = weekIndex > 0 ? weekTracking.get(weekIndex - 1) : new Set<string>();
const usedPositions = new Set<number>();
if (prevWeekEvents) {
prevWeekEvents.forEach(prevEventId => {
if (activeEvents.has(prevEventId)) {
usedPositions.add(positions.get(prevEventId) || 0);
}
});
}
let position = 0;
while (usedPositions.has(position)) {
position++;
}
positions.set(eventId, position);
}
});
});
return positions;
}, [weeks, getMultiDayEventsForDay]);
return (
<View style={[styles.scrollView, {width: screenWidth}]}>
<View style={styles.monthHeader}>
<Text style={styles.monthText}>{format(date, 'MMMM yyyy')}</Text>
<View style={styles.weekDayRow}>
{sortedDaysOfWeek.map((day, index) => (
<Text key={index} style={styles.weekDayText}>{day}</Text>
))}
</View>
</View>
<View style={styles.daysGrid}>
{weeks.map((week, weekIndex) => (
<React.Fragment key={weekIndex}>
{week.map((date, dayIndex) => {
const multiDayEvents = getMultiDayEventsForDay(date).map(event => ({
...event,
weekPosition: eventPositions.get(event.id) || 0
}));
return (
<Day
key={`${weekIndex}-${dayIndex}`}
date={date}
events={getEventsForDay(date)}
multiDayEvents={multiDayEvents}
dayWidth={dayWidth}
onPress={onPress}
/>
);
})}
</React.Fragment>
))}
</View>
</View>
);
});
const HEADER_HEIGHT = 40;
const styles = StyleSheet.create({
segmentslblStyle: {
fontSize: 12,
fontFamily: "Manrope_600SemiBold",
multiDayContainer: {
position: 'absolute',
top: 29,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
},
calHeader: {
borderWidth: 0,
paddingBottom: 0,
},
dayModeHeader: {
alignSelf: "flex-start",
justifyContent: "space-between",
alignContent: "center",
width: 38,
right: 42,
height: 13,
},
weekModeHeader: {},
monthModeHeader: {},
loadingContainer: {
dayContent: {
flex: 1,
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
zIndex: 100,
backgroundColor: "rgba(255, 255, 255, 0.9)",
padding: 4, // Move padding here instead
},
dayHeader: {
backgroundColor: "#4184f2",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
otherDayHeader: {
backgroundColor: "transparent",
color: "#919191",
aspectRatio: 1,
borderRadius: 100,
alignItems: "center",
justifyContent: "center",
},
hourStyle: {
color: "#5f6368",
fontSize: 12,
fontFamily: "Manrope_500Medium",
},
eventCell: {
eventsContainer: {
flex: 1,
marginTop: 2,
position: 'relative',
},
event: {
borderRadius: 4,
padding: 1,
marginVertical: 1,
height: 14,
},
eventText: {
fontSize: 10,
color: '#fff',
fontWeight: '500',
},
day: {
height: '14%',
padding: 0,
borderWidth: 0.5,
borderColor: '#eee',
position: 'relative',
overflow: 'visible',
},
container: {
flex: 1,
borderRadius: 5,
padding: 4,
height: '100%',
justifyContent: 'center',
},
eventTitle: {
color: 'white',
header: {
flexDirection: 'row',
justifyContent: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
},
scrollView: {
flex: 1,
},
monthContainer: {
flex: 1,
},
daysGrid: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
// justifyContent: 'center'
},
weekDay: {
alignItems: 'center',
justifyContent: 'center',
height: HEADER_HEIGHT,
},
scrollContent: {
flex: 1,
},
weekDayText: {
fontSize: 12,
fontFamily: "PlusJakartaSans_500Medium",
fontWeight: '600',
color: '#666',
},
dateContainer: {
minWidth: 20,
height: 24,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
},
todayContainer: {
backgroundColor: '#6200ee',
},
dateText: {
fontSize: 12,
fontWeight: '500',
color: '#333',
},
todayText: {
color: '#fff',
},
outsideMonthText: {
color: '#ccc',
},
moreEvents: {
fontSize: 10,
color: '#666',
textAlign: 'center',
},
monthHeader: {
paddingVertical: 12,
},
monthText: {
fontSize: 16,
fontWeight: '600',
color: '#333',
textAlign: 'center',
marginBottom: 8,
},
weekDayRow: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingHorizontal: 0,
},
});
export default MonthCalendar;

View File

@ -17,15 +17,34 @@ export const useCalendarControls = (events: any[]) => {
const handlePressEvent = useCallback((event: any) => {
const foundEvent = events?.find(x => x.id === event.id);
setEditVisible(true);
setEventForEdit(foundEvent!);
if (foundEvent) {
const processedEvent = {
...foundEvent,
startDate: foundEvent.startDate?.seconds ?
new Date(foundEvent.startDate.seconds * 1000) :
new Date(foundEvent.start),
endDate: foundEvent.endDate?.seconds ?
new Date(foundEvent.endDate.seconds * 1000) :
new Date(foundEvent.end)
};
if (processedEvent.startDate instanceof Date &&
processedEvent.endDate instanceof Date &&
!isNaN(processedEvent.startDate.getTime()) &&
!isNaN(processedEvent.endDate.getTime())) {
setEditVisible(true);
setEventForEdit(processedEvent);
}
}
}, [events, setEditVisible, setEventForEdit]);
const handlePressCell = useCallback((date: DateOrDateTime) => {
const selectedDate = new Date(date.dateTime!);
const minutes = selectedDate.getMinutes();
selectedDate.setMinutes(minutes - (minutes % 30), 0, 0); // Also sets seconds and milliseconds to 0
selectedDate.setMinutes(minutes - (minutes % 30), 0, 0);
setSelectedNewEndDate(selectedDate);
}, [setSelectedNewEndDate]);

View File

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

View File

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

View File

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

View File

@ -8,8 +8,8 @@ import type { AsyncPersistRetryer } from '@tanstack/query-async-storage-persiste
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 hours
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24,
staleTime: 1000 * 60 * 5,
retry: 2,
},
},

File diff suppressed because it is too large Load Diff

View File

@ -106,13 +106,13 @@ export const useGetEvents = () => {
const prefetchEvents = async () => {
await queryClient.prefetchQuery({
queryKey: ["events", user.uid, false], // Personal events
queryKey: ["events", user.uid, false],
queryFn: () => fetchEvents(user.uid, profileData, false),
staleTime: 5 * 60 * 1000,
});
await queryClient.prefetchQuery({
queryKey: ["events", user.uid, true], // Family events
queryKey: ["events", user.uid, true],
queryFn: () => fetchEvents(user.uid, profileData, true),
staleTime: 5 * 60 * 1000,
});
@ -150,8 +150,8 @@ export const useGetEvents = () => {
return useQuery({
queryKey: ["events", user?.uid, isFamilyView],
queryFn: () => fetchEvents(user?.uid!, profileData, isFamilyView),
staleTime: Infinity,
gcTime: Infinity,
staleTime: 10 * 60 * 1000,
gcTime: 24 * 60 * 60 * 1000,
placeholderData: (previousData) => previousData,
enabled: Boolean(user?.uid),
});

4001
ios/Podfile.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -52,6 +52,7 @@
"@react-native/assets-registry": "^0.76.3",
"@react-navigation/drawer": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@shopify/flash-list": "^1.7.2",
"@tanstack/query-async-storage-persister": "^5.62.7",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-persist-client": "^5.62.7",
@ -89,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",

View File

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

View File

@ -3226,6 +3226,14 @@
component-type "^1.2.1"
join-component "^1.1.0"
"@shopify/flash-list@^1.7.2":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.7.2.tgz#a862515a6aae912486d4515909761320d6a6e964"
integrity sha512-rnadpDht/mlJLDM02HBni49EJJQhc51zjJ3diGnTz3MV6U8vK9Hztou+2C5d6bNLb4oZvSG5f7NTWejkipyMLw==
dependencies:
recyclerlistview "4.2.1"
tslib "2.6.3"
"@sideway/address@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
@ -4386,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"
@ -4997,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==
@ -9610,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"
@ -9974,7 +9969,7 @@ recast@^0.21.0:
source-map "~0.6.1"
tslib "^2.0.1"
recyclerlistview@^4.0.0:
recyclerlistview@4.2.1, recyclerlistview@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13"
integrity sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g==
@ -11143,6 +11138,11 @@ ts-object-utils@0.0.5:
resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077"
integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==
tslib@2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"