mirror of
https://github.com/urosran/cally.git
synced 2026-03-11 05:41:43 +00:00
Compare commits
26 Commits
dev
...
7a7b1902a8
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7b1902a8 | |||
| c184eb3293 | |||
| d9ee1cd921 | |||
| b804f37037 | |||
| 718fd562ff | |||
| a855e6d3bf | |||
| e04441bd81 | |||
| f9a5e76162 | |||
| 479e7c5f56 | |||
| 3d15d7bb74 | |||
| 300ce82a4d | |||
| 5ac4526baf | |||
| 5cfec1544a | |||
| 0998dc29d0 | |||
| dc7d59eecc | |||
| 14be80c6f0 | |||
| 580104d052 | |||
| 231e99ff8f | |||
| bc77c403f2 | |||
| 1d4903b609 | |||
| 60953c34bc | |||
| adafaf1dfe | |||
| 0f07e46a82 | |||
| 49be903553 | |||
| 8b6171a819 | |||
| 28110b88cc |
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,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>
|
||||
|
||||
@ -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;
|
||||
@ -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 =
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,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",
|
||||
},
|
||||
|
||||
@ -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,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",
|
||||
},
|
||||
});
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
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) && (
|
||||
{(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);
|
||||
@ -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}}
|
||||
|
||||
@ -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;
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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
@ -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
4001
ios/Podfile.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
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 };
|
||||
30
yarn.lock
30
yarn.lock
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user