mirror of
https://github.com/urosran/cally.git
synced 2025-07-10 07:07:16 +00:00
Notification changes
This commit is contained in:
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="t" enabled="true" level="TEXT ATTRIBUTES" enabled_by_default="true" editorAttributes="CONSIDERATION_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EslintConfiguration">
|
||||
<option name="fix-on-save" value="true" />
|
||||
</component>
|
||||
</project>
|
5
app.json
5
app.json
@ -17,7 +17,10 @@
|
||||
"bundleIdentifier": "com.cally.app",
|
||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||
"buildNumber": "74",
|
||||
"usesAppleSignIn": true
|
||||
"usesAppleSignIn": true,
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
|
@ -1,15 +1,9 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Drawer } from "expo-router/drawer";
|
||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
||||
import { DrawerContentScrollView } from "@react-navigation/drawer";
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import { ImageBackground, StyleSheet } from "react-native";
|
||||
import React, {useMemo} from "react";
|
||||
import {Drawer} from "expo-router/drawer";
|
||||
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||
import {DrawerContentScrollView, DrawerNavigationOptions, DrawerNavigationProp} from "@react-navigation/drawer";
|
||||
import {Button, ButtonSize, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||
import {ImageBackground, StyleSheet} from "react-native";
|
||||
import DrawerButton from "@/components/shared/DrawerButton";
|
||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
@ -17,127 +11,169 @@ 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 {useSetAtom} from "jotai";
|
||||
import {
|
||||
isFamilyViewAtom,
|
||||
settingsPageIndex,
|
||||
toDosPageIndex,
|
||||
userSettingsView,
|
||||
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 {DeviceType} from "expo-device";
|
||||
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
||||
import {RouteProp} from "@react-navigation/core";
|
||||
|
||||
type DrawerParamList = {
|
||||
index: undefined;
|
||||
calendar: undefined;
|
||||
todos: undefined;
|
||||
};
|
||||
|
||||
type NavigationProp = DrawerNavigationProp<DrawerParamList>;
|
||||
|
||||
interface ViewSwitchProps {
|
||||
navigation: NavigationProp;
|
||||
}
|
||||
|
||||
interface HeaderRightProps {
|
||||
routeName: keyof DrawerParamList;
|
||||
navigation: NavigationProp;
|
||||
}
|
||||
|
||||
const MemoizedViewSwitch = React.memo<ViewSwitchProps>(({navigation}) => (
|
||||
<View marginR-16>
|
||||
<ViewSwitch navigation={navigation}/>
|
||||
</View>
|
||||
));
|
||||
|
||||
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 {mutateAsync: signOut} = useSignOut();
|
||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||
const setUserView = useSetAtom(userSettingsView);
|
||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
initialRouteName={"index"}
|
||||
detachInactiveScreens
|
||||
screenOptions={({ navigation, route }) => ({
|
||||
headerShown: true,
|
||||
headerTitleAlign:
|
||||
Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
||||
headerTitleStyle: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||
},
|
||||
headerLeft: (props) => (
|
||||
<TouchableOpacity
|
||||
onPress={navigation.toggleDrawer}
|
||||
style={{ marginLeft: 16 }}
|
||||
>
|
||||
<DrawerIcon />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => {
|
||||
// Only show ViewSwitch on calendar and todos pages
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(
|
||||
route.name
|
||||
);
|
||||
return Device.deviceType === DeviceType.TABLET && showViewSwitch ? (
|
||||
<View marginR-16>
|
||||
<ViewSwitch navigation={navigation} />
|
||||
</View>
|
||||
) : null;
|
||||
},
|
||||
drawerStyle: {
|
||||
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
|
||||
backgroundColor: "#f9f8f7",
|
||||
height: "100%",
|
||||
},
|
||||
})}
|
||||
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
|
||||
const screenOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
navigation,
|
||||
route,
|
||||
}: {
|
||||
navigation: DrawerNavigationProp<DrawerParamList>;
|
||||
route: RouteProp<DrawerParamList>;
|
||||
}): DrawerNavigationOptions => ({
|
||||
headerShown: true,
|
||||
headerTitleAlign: Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
||||
headerTitleStyle: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={navigation.toggleDrawer}
|
||||
style={{marginLeft: 16}}
|
||||
>
|
||||
<DrawerIcon/>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => {
|
||||
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
|
||||
|
||||
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MemoizedViewSwitch navigation={navigation}/>;
|
||||
},
|
||||
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"}
|
||||
@ -150,184 +186,184 @@ export default function TabLayout() {
|
||||
/>
|
||||
}
|
||||
/>*/}
|
||||
<DrawerButton
|
||||
color="#8005eb"
|
||||
title={"To Do's"}
|
||||
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 }}
|
||||
/>
|
||||
<DrawerButton
|
||||
color="#8005eb"
|
||||
title={"To Do's"}
|
||||
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()}
|
||||
<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:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family Calendar"
|
||||
: "Calendar",
|
||||
}}
|
||||
/>
|
||||
</DrawerContentScrollView>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Drawer.Screen
|
||||
name="index"
|
||||
options={{
|
||||
drawerLabel: "Calendar",
|
||||
title:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family Calendar"
|
||||
: "Calendar",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="calendar"
|
||||
options={{
|
||||
drawerLabel: "Calendar",
|
||||
title:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family Calendar"
|
||||
: "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: "Grocery",
|
||||
title: "Grocery",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="reminders"
|
||||
options={{
|
||||
drawerLabel: "Reminders",
|
||||
title: "Reminders",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="todos"
|
||||
options={{
|
||||
drawerLabel: "To-Do",
|
||||
title:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family To Do's"
|
||||
: "To Do's",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="notifications"
|
||||
options={{
|
||||
drawerLabel: "Notifications",
|
||||
title: "Notifications",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="feedback"
|
||||
options={{ drawerLabel: "Feedback", title: "Feedback" }}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
<Drawer.Screen
|
||||
name="calendar"
|
||||
options={{
|
||||
drawerLabel: "Calendar",
|
||||
title:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family Calendar"
|
||||
: "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: "Grocery",
|
||||
title: "Grocery",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="reminders"
|
||||
options={{
|
||||
drawerLabel: "Reminders",
|
||||
title: "Reminders",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="todos"
|
||||
options={{
|
||||
drawerLabel: "To-Do",
|
||||
title:
|
||||
Device.deviceType === DeviceType.TABLET
|
||||
? "Family To Do's"
|
||||
: "To Do's",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="notifications"
|
||||
options={{
|
||||
drawerLabel: "Notifications",
|
||||
title: "Notifications",
|
||||
}}
|
||||
/>
|
||||
<Drawer.Screen
|
||||
name="feedback"
|
||||
options={{drawerLabel: "Feedback", title: "Feedback"}}
|
||||
/>
|
||||
</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",
|
||||
},
|
||||
signOut: {fontFamily: "Poppins_500Medium", fontSize: 15},
|
||||
label: {fontFamily: "Poppins_400Medium", fontSize: 15},
|
||||
title: {
|
||||
fontSize: 26.13,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
color: "#262627",
|
||||
},
|
||||
});
|
||||
|
@ -1,105 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import { ScrollView, RefreshControl, View } from "react-native";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import {RefreshControl, ScrollView, View} from "react-native";
|
||||
import CalendarPage from "@/components/pages/calendar/CalendarPage";
|
||||
import { refreshTriggerAtom } from "@/components/pages/calendar/atoms";
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import {colorMap} from "@/constants/colorMap";
|
||||
import TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
|
||||
import { DeviceType } from "expo-device";
|
||||
import * as Device from "expo-device";
|
||||
import { useCalSync } from "@/hooks/useCalSync";
|
||||
import Toast from "react-native-toast-message";
|
||||
import {DeviceType} from "expo-device";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
|
||||
export default function Screen() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [shouldRefresh, setShouldRefresh] = useAtom(refreshTriggerAtom);
|
||||
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||
const {
|
||||
resyncAllCalendars,
|
||||
isSyncing,
|
||||
} = useCalSync();
|
||||
|
||||
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
try {
|
||||
await resyncAllCalendars();
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
}
|
||||
}, [resyncAllCalendars]);
|
||||
|
||||
const {
|
||||
isConnectedToGoogle,
|
||||
isConnectedToMicrosoft,
|
||||
isConnectedToApple,
|
||||
resyncAllCalendars,
|
||||
isSyncing,
|
||||
} = useCalSync();
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
|
||||
const minimumDelay = new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
try {
|
||||
if (isConnectedToGoogle || isConnectedToMicrosoft || isConnectedToApple) {
|
||||
await Promise.all([resyncAllCalendars(), minimumDelay]);
|
||||
} else {
|
||||
await minimumDelay;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Refresh failed:", error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setShouldRefresh((prev) => !prev);
|
||||
}
|
||||
}, [
|
||||
resyncAllCalendars,
|
||||
isConnectedToGoogle,
|
||||
isConnectedToMicrosoft,
|
||||
isConnectedToApple,
|
||||
]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, zIndex: 0 }}>
|
||||
{Device.deviceType === DeviceType.TABLET ? (
|
||||
<TabletCalendarPage />
|
||||
) : (
|
||||
<CalendarPage />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: isTablet ? "15%" : "0",
|
||||
height: isTablet ? "9%" : "4%",
|
||||
width: isTablet ? "62%" : "100%",
|
||||
zIndex: 50,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingRight: 200,
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
const refreshControl = (
|
||||
<RefreshControl
|
||||
colors={[
|
||||
colorMap.pink,
|
||||
colorMap.green,
|
||||
colorMap.orange,
|
||||
colorMap.purple,
|
||||
colorMap.teal,
|
||||
colorMap.pink,
|
||||
colorMap.green,
|
||||
colorMap.orange,
|
||||
colorMap.purple,
|
||||
colorMap.teal,
|
||||
]}
|
||||
tintColor={colorMap.pink}
|
||||
progressBackgroundColor={"white"}
|
||||
refreshing={refreshing || isSyncing}
|
||||
progressBackgroundColor="white"
|
||||
refreshing={isSyncing}
|
||||
onRefresh={onRefresh}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%", // Position at screen center
|
||||
transform: [
|
||||
// Offset by half its own width
|
||||
{ translateX: -20 }, // Assuming the refresh control is ~40px wide
|
||||
],
|
||||
}}
|
||||
/>
|
||||
}
|
||||
bounces={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pointerEvents={refreshing || isSyncing ? "auto" : "none"}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{flex: 1, height: "100%",}}
|
||||
contentContainerStyle={{flex: 1, height: "100%"}}
|
||||
refreshControl={refreshControl}
|
||||
bounces={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
{isTablet ? <TabletCalendarPage/> : <CalendarPage/>}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
@ -1,25 +1,16 @@
|
||||
import TabletChoresPage from "@/components/pages/(tablet_pages)/chores/TabletChoresPage";
|
||||
import AddChore from "@/components/pages/todos/AddChore";
|
||||
import ProgressCard from "@/components/pages/todos/ProgressCard";
|
||||
import ToDoItem from "@/components/pages/todos/ToDoItem";
|
||||
import ToDosList from "@/components/pages/todos/ToDosList";
|
||||
import ToDosPage from "@/components/pages/todos/ToDosPage";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { ToDosContextProvider, useToDosContext } from "@/contexts/ToDosContext";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { Button, ButtonSize, View, Text, Constants } from "react-native-ui-lib";
|
||||
import {ToDosContextProvider} from "@/contexts/ToDosContext";
|
||||
import * as Device from "expo-device";
|
||||
|
||||
export default function Screen() {
|
||||
return (
|
||||
<ToDosContextProvider>
|
||||
{Device.deviceType === Device.DeviceType.TABLET ? (
|
||||
<TabletChoresPage />
|
||||
) : (
|
||||
<ToDosPage />
|
||||
)}
|
||||
</ToDosContextProvider>
|
||||
);
|
||||
return (
|
||||
<ToDosContextProvider>
|
||||
{Device.deviceType === Device.DeviceType.TABLET ? (
|
||||
<TabletChoresPage/>
|
||||
) : (
|
||||
<ToDosPage/>
|
||||
)}
|
||||
</ToDosContextProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,89 +1,68 @@
|
||||
import { SegmentedControl, View } from "react-native-ui-lib";
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { memo, useCallback } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { NavigationProp, useNavigationState } from "@react-navigation/native";
|
||||
|
||||
const ViewSwitch = memo(function ViewSwitch({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: any;
|
||||
}) {
|
||||
const isNavigating = useRef(false);
|
||||
const navigationState = useNavigationState((state) => state);
|
||||
const [selectedIndex, setSelectedIndex] = useState(
|
||||
navigationState.index === 6 ? 1 : 0
|
||||
);
|
||||
interface ViewSwitchProps {
|
||||
navigation: NavigationProp<any>;
|
||||
}
|
||||
|
||||
// Sync the selected index with navigation state
|
||||
useEffect(() => {
|
||||
const newIndex = navigationState.index === 6 ? 1 : 0;
|
||||
if (selectedIndex !== newIndex) {
|
||||
setSelectedIndex(newIndex);
|
||||
}
|
||||
isNavigating.current = false;
|
||||
}, [navigationState.index]);
|
||||
const ViewSwitch = memo(function ViewSwitch({ navigation }: ViewSwitchProps) {
|
||||
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
|
||||
|
||||
const handleSegmentChange = useCallback(
|
||||
(index: number) => {
|
||||
if (isNavigating.current) return;
|
||||
if (index === selectedIndex) return;
|
||||
const handleSegmentChange = useCallback(
|
||||
(index: number) => {
|
||||
if (index === currentIndex) return;
|
||||
navigation.navigate(index === 0 ? "calendar" : "todos");
|
||||
},
|
||||
[navigation, currentIndex]
|
||||
);
|
||||
|
||||
isNavigating.current = true;
|
||||
setSelectedIndex(index);
|
||||
|
||||
// Delay navigation slightly to allow the segment control to update
|
||||
requestAnimationFrame(() => {
|
||||
navigation.navigate(index === 0 ? "calendar" : "todos");
|
||||
});
|
||||
console.log(selectedIndex)
|
||||
},
|
||||
[navigation, selectedIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SegmentedControl
|
||||
segments={[
|
||||
{ label: "Calendar", segmentLabelStyle: styles.labelStyle },
|
||||
{ label: "To Do's", segmentLabelStyle: styles.labelStyle },
|
||||
]}
|
||||
containerStyle={styles.segmentContainer}
|
||||
style={styles.segment}
|
||||
backgroundColor="#ebebeb"
|
||||
inactiveColor="black"
|
||||
activeColor="white"
|
||||
activeBackgroundColor="#ea156c"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={selectedIndex}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SegmentedControl
|
||||
segments={[
|
||||
{ label: "Calendar", segmentLabelStyle: styles.labelStyle },
|
||||
{ label: "To Do's", segmentLabelStyle: styles.labelStyle },
|
||||
]}
|
||||
containerStyle={styles.segmentContainer}
|
||||
style={styles.segment}
|
||||
backgroundColor="#ebebeb"
|
||||
inactiveColor="black"
|
||||
activeColor="white"
|
||||
activeBackgroundColor="#ea156c"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={currentIndex}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
export default ViewSwitch;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#ebebeb",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
segmentContainer: {
|
||||
height: 44,
|
||||
width: 220,
|
||||
},
|
||||
segment: {
|
||||
borderRadius: 50,
|
||||
borderWidth: 0,
|
||||
height: 44,
|
||||
},
|
||||
labelStyle: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
container: {
|
||||
borderRadius: 30,
|
||||
backgroundColor: "#ebebeb",
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0,
|
||||
shadowRadius: 0,
|
||||
elevation: 0,
|
||||
},
|
||||
segmentContainer: {
|
||||
height: 44,
|
||||
width: 220,
|
||||
},
|
||||
segment: {
|
||||
borderRadius: 50,
|
||||
borderWidth: 0,
|
||||
height: 44,
|
||||
},
|
||||
labelStyle: {
|
||||
fontSize: 16,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
|
||||
export default ViewSwitch;
|
@ -1,92 +1,89 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Text } from "react-native-ui-lib";
|
||||
import React, {useEffect} from "react";
|
||||
import {Text, View} from "react-native-ui-lib";
|
||||
import * as ScreenOrientation from "expo-screen-orientation";
|
||||
import TabletContainer from "../tablet_components/TabletContainer";
|
||||
import ToDosPage from "../../todos/ToDosPage";
|
||||
import ToDosList from "../../todos/ToDosList";
|
||||
import SingleUserChoreList from "./SingleUserChoreList";
|
||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import { ImageBackground, StyleSheet } from "react-native";
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import {ImageBackground, StyleSheet} from "react-native";
|
||||
import {ScrollView} from "react-native-gesture-handler";
|
||||
|
||||
const TabletChoresPage = () => {
|
||||
const { data: users } = useGetFamilyMembers();
|
||||
// Function to lock the screen orientation to landscape
|
||||
const lockScreenOrientation = async () => {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
lockScreenOrientation(); // Lock orientation when the component mounts
|
||||
|
||||
return () => {
|
||||
// Optional: Unlock to default when the component unmounts
|
||||
ScreenOrientation.unlockAsync();
|
||||
const {data: users} = useGetFamilyMembers();
|
||||
// Function to lock the screen orientation to landscape
|
||||
const lockScreenOrientation = async () => {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabletContainer>
|
||||
<ScrollView horizontal>
|
||||
<View row gap-25 padding-25>
|
||||
{users?.map((user, index) => (
|
||||
<View>
|
||||
<View row centerV>
|
||||
{user.pfp ? (
|
||||
<ImageBackground
|
||||
source={{ uri: user.pfp }}
|
||||
style={[
|
||||
styles.pfp,
|
||||
(user.eventColor && {
|
||||
borderWidth: 2,
|
||||
borderColor: user.eventColor,
|
||||
}) ||
|
||||
undefined,
|
||||
]}
|
||||
borderRadius={13.33}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
center
|
||||
style={styles.pfp}
|
||||
backgroundColor={user.eventColor || "#00a8b6"}
|
||||
>
|
||||
<Text color="white">
|
||||
{user.firstName.at(0)}
|
||||
{user.lastName.at(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.name} marginL-15>
|
||||
{user.firstName}
|
||||
</Text>
|
||||
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
|
||||
({user.userType})
|
||||
</Text>
|
||||
</View>
|
||||
<SingleUserChoreList user={user} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</TabletContainer>
|
||||
);
|
||||
useEffect(() => {
|
||||
lockScreenOrientation(); // Lock orientation when the component mounts
|
||||
|
||||
return () => {
|
||||
// Optional: Unlock to default when the component unmounts
|
||||
ScreenOrientation.unlockAsync();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabletContainer>
|
||||
<ScrollView horizontal>
|
||||
<View row gap-25 padding-25>
|
||||
{users?.map((user, index) => (
|
||||
<View>
|
||||
<View row centerV>
|
||||
{user.pfp ? (
|
||||
<ImageBackground
|
||||
source={{uri: user.pfp}}
|
||||
style={[
|
||||
styles.pfp,
|
||||
(user.eventColor && {
|
||||
borderWidth: 2,
|
||||
borderColor: user.eventColor,
|
||||
}) ||
|
||||
undefined,
|
||||
]}
|
||||
borderRadius={13.33}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
center
|
||||
style={styles.pfp}
|
||||
backgroundColor={user.eventColor || "#00a8b6"}
|
||||
>
|
||||
<Text color="white">
|
||||
{user.firstName.at(0)}
|
||||
{user.lastName.at(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text style={styles.name} marginL-15>
|
||||
{user.firstName}
|
||||
</Text>
|
||||
<Text style={[styles.name, {color: "#9b9b9b"}]} marginL-5>
|
||||
({user.userType})
|
||||
</Text>
|
||||
</View>
|
||||
<SingleUserChoreList user={user}/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</TabletContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pfp: {
|
||||
width: 46.74,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 13.33,
|
||||
},
|
||||
name: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 22.43,
|
||||
color: "#2c2c2c",
|
||||
},
|
||||
pfp: {
|
||||
width: 46.74,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 13.33,
|
||||
},
|
||||
name: {
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
fontSize: 22.43,
|
||||
color: "#2c2c2c",
|
||||
},
|
||||
});
|
||||
|
||||
export default TabletChoresPage;
|
||||
|
@ -3,7 +3,6 @@ import React from "react";
|
||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||
import { FlatList } from "react-native";
|
||||
import BrainDumpItem from "./DumpItem";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
|
||||
const DumpList = (props: { searchText: string }) => {
|
||||
const { brainDumps } = useBrainDumpContext();
|
||||
|
@ -1,131 +1,121 @@
|
||||
import React, { memo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Picker,
|
||||
PickerModes,
|
||||
SegmentedControl,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native-ui-lib";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { modeMap, 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 { useAuthContext } from "@/contexts/AuthContext";
|
||||
import {useIsMutating} from "react-query";
|
||||
import React, {memo} from "react";
|
||||
import {Button, Picker, PickerModes, SegmentedControl, Text, View,} from "react-native-ui-lib";
|
||||
import {MaterialIcons} from "@expo/vector-icons";
|
||||
import {modeMap, 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";
|
||||
|
||||
export const CalendarHeader = memo(() => {
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const { profileData } = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
|
||||
const handleSegmentChange = (index: number) => {
|
||||
const selectedMode = modeMap.get(index);
|
||||
if (selectedMode) {
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode as "day" | "week" | "month");
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
const handleSegmentChange = (index: number) => {
|
||||
const selectedMode = modeMap.get(index);
|
||||
if (selectedMode) {
|
||||
setTimeout(() => {
|
||||
setMode(selectedMode as "day" | "week" | "month");
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMonthChange = (month: string) => {
|
||||
const currentDay = selectedDate.getDate();
|
||||
const currentYear = selectedDate.getFullYear();
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
const handleMonthChange = (month: string) => {
|
||||
const currentDay = selectedDate.getDate();
|
||||
const currentYear = selectedDate.getFullYear();
|
||||
const newMonthIndex = months.indexOf(month);
|
||||
|
||||
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
|
||||
setSelectedDate(updatedDate);
|
||||
};
|
||||
const updatedDate = new Date(currentYear, newMonthIndex, currentDay);
|
||||
setSelectedDate(updatedDate);
|
||||
};
|
||||
|
||||
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: "white",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<View row centerV gap-3>
|
||||
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder={"Select Month"}
|
||||
style={{ fontFamily: "Manrope_500Medium", fontSize: 17, width: 85 }}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 },
|
||||
}}
|
||||
>
|
||||
{months.map((month) => (
|
||||
<Picker.Item key={month} label={month} value={month} />
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View row centerV>
|
||||
{!isSelectedDateToday && (
|
||||
<Button
|
||||
size={"xSmall"}
|
||||
marginR-0
|
||||
avoidInnerPadding
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
backgroundColor: "white",
|
||||
borderWidth: 0.7,
|
||||
borderColor: "#dadce0",
|
||||
height: 30,
|
||||
paddingHorizontal: 10,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
backgroundColor: "white",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
labelStyle={{
|
||||
fontSize: 12,
|
||||
color: "black",
|
||||
fontFamily: "Manrope_500Medium",
|
||||
}}
|
||||
label={format(new Date(), "dd/MM/yyyy")}
|
||||
onPress={() => {
|
||||
setSelectedDate(new Date());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<View row centerV gap-3>
|
||||
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
|
||||
{selectedDate.getFullYear()}
|
||||
</Text>
|
||||
<Picker
|
||||
value={months[selectedDate.getMonth()]}
|
||||
placeholder={"Select Month"}
|
||||
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
|
||||
mode={PickerModes.SINGLE}
|
||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
|
||||
topBarProps={{
|
||||
title: selectedDate.getFullYear().toString(),
|
||||
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
|
||||
}}
|
||||
>
|
||||
{months.map((month) => (
|
||||
<Picker.Item key={month} label={month} value={month}/>
|
||||
))}
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<SegmentedControl
|
||||
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentslblStyle}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
|
||||
/>
|
||||
<View row centerV>
|
||||
{!isSelectedDateToday && (
|
||||
<Button
|
||||
size={"xSmall"}
|
||||
marginR-0
|
||||
avoidInnerPadding
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
backgroundColor: "white",
|
||||
borderWidth: 0.7,
|
||||
borderColor: "#dadce0",
|
||||
height: 30,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
labelStyle={{
|
||||
fontSize: 12,
|
||||
color: "black",
|
||||
fontFamily: "Manrope_500Medium",
|
||||
}}
|
||||
label={format(new Date(), "dd/MM/yyyy")}
|
||||
onPress={() => {
|
||||
setSelectedDate(new Date());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<SegmentedControl
|
||||
segments={[{label: "D"}, {label: "W"}, {label: "M"}]}
|
||||
backgroundColor="#ececec"
|
||||
inactiveColor="#919191"
|
||||
activeBackgroundColor="#ea156c"
|
||||
activeColor="white"
|
||||
outlineColor="white"
|
||||
outlineWidth={3}
|
||||
segmentLabelStyle={styles.segmentslblStyle}
|
||||
onChangeIndex={handleSegmentChange}
|
||||
initialIndex={mode === "day" ? 0 : mode === "week" ? 1 : 2}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
segmentslblStyle: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
segmentslblStyle: {
|
||||
fontSize: 12,
|
||||
fontFamily: "Manrope_600SemiBold",
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native-ui-lib";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
|
||||
|
||||
export default function CalendarPage() {
|
||||
|
@ -1,61 +1,60 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Calendar } from "react-native-big-calendar";
|
||||
import { ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle } from "react-native";
|
||||
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {Calendar} from "react-native-big-calendar";
|
||||
import {ActivityIndicator, StyleSheet, View, ViewStyle} from "react-native";
|
||||
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||
import {useAtom, useSetAtom} from "jotai";
|
||||
import {
|
||||
editVisibleAtom,
|
||||
eventForEditAtom,
|
||||
isAllDayAtom, isFamilyViewAtom,
|
||||
isAllDayAtom,
|
||||
isFamilyViewAtom,
|
||||
modeAtom,
|
||||
refreshTriggerAtom,
|
||||
selectedDateAtom,
|
||||
selectedNewEventDateAtom,
|
||||
} 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, isWithinInterval, subDays } from "date-fns";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
||||
import {Text} from "react-native-ui-lib";
|
||||
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
|
||||
import {useCalSync} from "@/hooks/useCalSync";
|
||||
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
||||
import {colorMap} from "@/constants/colorMap";
|
||||
|
||||
interface EventCalendarProps {
|
||||
calendarHeight: number;
|
||||
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
|
||||
calendarWidth: number;
|
||||
calendarHeight: number;
|
||||
// WAS USED FOR SCROLLABLE CALENDARS, PERFORMANCE WAS NOT OPTIMAL
|
||||
calendarWidth: number;
|
||||
}
|
||||
|
||||
const getTotalMinutes = () => {
|
||||
const date = new Date();
|
||||
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
||||
const date = new Date();
|
||||
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
||||
};
|
||||
|
||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
({ calendarHeight }) => {
|
||||
const { data: events, isLoading, refetch } = useGetEvents();
|
||||
const { profileData, user } = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||
({calendarHeight}) => {
|
||||
const {data: events, isLoading, refetch} = useGetEvents();
|
||||
const {profileData, user} = useAuthContext();
|
||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||
const [mode, setMode] = useAtom(modeAtom);
|
||||
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||
|
||||
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||
const shouldRefresh = useAtomValue(refreshTriggerAtom);
|
||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||
|
||||
const {isSyncing} = useSyncEvents()
|
||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||
useCalSync()
|
||||
const {isSyncing} = useSyncEvents()
|
||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||
useCalSync()
|
||||
|
||||
const todaysDate = new Date();
|
||||
const todaysDate = new Date();
|
||||
|
||||
const handlePressEvent = useCallback(
|
||||
(event: CalendarEvent) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setEditVisible(true);
|
||||
// console.log({event});
|
||||
const handlePressEvent = useCallback(
|
||||
(event: CalendarEvent) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setEditVisible(true);
|
||||
// console.log({event});
|
||||
setEventForEdit(event);
|
||||
} else {
|
||||
setMode("day");
|
||||
@ -65,209 +64,195 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
[setEditVisible, setEventForEdit, mode]
|
||||
);
|
||||
|
||||
const handlePressCell = useCallback(
|
||||
(date: Date) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setSelectedNewEndDate(date);
|
||||
} else {
|
||||
setMode("day");
|
||||
setSelectedDate(date);
|
||||
}
|
||||
},
|
||||
[mode, setSelectedNewEndDate, setSelectedDate]
|
||||
);
|
||||
const handlePressCell = useCallback(
|
||||
(date: Date) => {
|
||||
if (mode === "day" || mode === "week") {
|
||||
setSelectedNewEndDate(date);
|
||||
} else {
|
||||
setMode("day");
|
||||
setSelectedDate(date);
|
||||
}
|
||||
},
|
||||
[mode, setSelectedNewEndDate, setSelectedDate]
|
||||
);
|
||||
|
||||
const handlePressDayHeader = useCallback(
|
||||
(date: Date) => {
|
||||
if (mode === "day") {
|
||||
setIsAllDay(true);
|
||||
setSelectedNewEndDate(date);
|
||||
setEditVisible(true);
|
||||
}
|
||||
if (mode === 'week')
|
||||
{
|
||||
setSelectedDate(date)
|
||||
const handlePressDayHeader = useCallback(
|
||||
(date: Date) => {
|
||||
if (mode === "day") {
|
||||
setIsAllDay(true);
|
||||
setSelectedNewEndDate(date);
|
||||
setEditVisible(true);
|
||||
}
|
||||
if (mode === 'week') {
|
||||
setSelectedDate(date)
|
||||
|
||||
setMode("day")
|
||||
}
|
||||
},
|
||||
[mode, setSelectedNewEndDate]
|
||||
);
|
||||
setMode("day")
|
||||
}
|
||||
},
|
||||
[mode, setSelectedNewEndDate]
|
||||
);
|
||||
|
||||
const handleSwipeEnd = useCallback(
|
||||
(date: Date) => {
|
||||
setSelectedDate(date);
|
||||
},
|
||||
[setSelectedDate]
|
||||
);
|
||||
const handleSwipeEnd = useCallback(
|
||||
(date: Date) => {
|
||||
setSelectedDate(date);
|
||||
},
|
||||
[setSelectedDate]
|
||||
);
|
||||
|
||||
const memoizedEventCellStyle = useCallback(
|
||||
(event: CalendarEvent) => {
|
||||
let eventColor = event.eventColor;
|
||||
if (!isFamilyView && event.attendees?.includes(user?.uid)) {
|
||||
eventColor = profileData?.eventColor ?? colorMap.teal;
|
||||
}
|
||||
const memoizedEventCellStyle = useCallback(
|
||||
(event: CalendarEvent) => {
|
||||
let eventColor = event.eventColor;
|
||||
if (!isFamilyView && event.attendees?.includes(user?.uid)) {
|
||||
eventColor = profileData?.eventColor ?? colorMap.teal;
|
||||
}
|
||||
|
||||
return { backgroundColor: eventColor , fontSize: 14}
|
||||
},
|
||||
[]
|
||||
);
|
||||
return {backgroundColor: eventColor, fontSize: 14}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const memoizedWeekStartsOn = useMemo(
|
||||
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
|
||||
[profileData]
|
||||
);
|
||||
const memoizedWeekStartsOn = useMemo(
|
||||
() => (profileData?.firstDayOfWeek === "Mondays" ? 1 : 0),
|
||||
[profileData]
|
||||
);
|
||||
|
||||
// console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek,
|
||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getDate() === date2.getDate() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getFullYear() === date2.getFullYear()
|
||||
);
|
||||
}, []);
|
||||
|
||||
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 dayHeaderColor = useMemo(() => {
|
||||
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
|
||||
}, [selectedDate, mode]);
|
||||
const dateStyle = useMemo(() => {
|
||||
if (mode === "week") return undefined;
|
||||
return isSameDate(todaysDate, selectedDate) && mode === "day"
|
||||
? styles.dayHeader
|
||||
: styles.otherDayHeader;
|
||||
}, [selectedDate, mode]);
|
||||
|
||||
const dateStyle = useMemo(() => {
|
||||
if (mode === "week") 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") {
|
||||
return styles.weekModeHeader;
|
||||
} else if (mode === "month") {
|
||||
return styles.monthModeHeader;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
const memoizedHeaderContentStyle = useMemo(() => {
|
||||
if (mode === "day") {
|
||||
return styles.dayModeHeader;
|
||||
} else if (mode === "week") {
|
||||
return styles.weekModeHeader;
|
||||
} else if (mode === "month") {
|
||||
return styles.monthModeHeader;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}, [mode]);
|
||||
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||
const startTime = Date.now(); // Start timer
|
||||
|
||||
const { enrichedEvents, filteredEvents } = useMemo(() => {
|
||||
const startTime = Date.now(); // Start timer
|
||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
const endOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
|
||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
const endOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||
const filteredEvents =
|
||||
events?.filter(
|
||||
(event) =>
|
||||
event.start &&
|
||||
event.end &&
|
||||
isWithinInterval(event.start, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
}) &&
|
||||
isWithinInterval(event.end, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
})
|
||||
) ?? [];
|
||||
|
||||
const filteredEvents =
|
||||
events?.filter(
|
||||
(event) =>
|
||||
event.start &&
|
||||
event.end &&
|
||||
isWithinInterval(event.start, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
}) &&
|
||||
isWithinInterval(event.end, {
|
||||
start: subDays(selectedDate, startOffset),
|
||||
end: addDays(selectedDate, endOffset),
|
||||
})
|
||||
) ?? [];
|
||||
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
||||
const dateKey = event.start.toISOString().split("T")[0];
|
||||
acc[dateKey] = acc[dateKey] || [];
|
||||
acc[dateKey].push({
|
||||
...event,
|
||||
overlapPosition: false,
|
||||
overlapCount: 0,
|
||||
});
|
||||
|
||||
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
||||
const dateKey = event.start.toISOString().split("T")[0];
|
||||
acc[dateKey] = acc[dateKey] || [];
|
||||
acc[dateKey].push({
|
||||
...event,
|
||||
overlapPosition: false,
|
||||
overlapCount: 0,
|
||||
});
|
||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||
|
||||
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||
return acc;
|
||||
}, {} as Record<string, CalendarEvent[]>);
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, CalendarEvent[]>);
|
||||
const endTime = Date.now();
|
||||
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||
|
||||
const endTime = Date.now();
|
||||
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||
return {enrichedEvents, filteredEvents};
|
||||
}, [events, selectedDate, mode]);
|
||||
|
||||
return { enrichedEvents, filteredEvents };
|
||||
}, [events, selectedDate, mode]);
|
||||
const renderCustomDateForMonth = (date: Date) => {
|
||||
const circleStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: 15,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const renderCustomDateForMonth = (date: Date) => {
|
||||
const circleStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderRadius: 15,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const defaultStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
|
||||
const defaultStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
const currentDateStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
backgroundColor: "#4184f2",
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
|
||||
const currentDateStyle = useMemo<ViewStyle>(
|
||||
() => ({
|
||||
...circleStyle,
|
||||
backgroundColor: "#4184f2",
|
||||
}),
|
||||
[circleStyle]
|
||||
);
|
||||
const renderDate = useCallback(
|
||||
(date: Date) => {
|
||||
const isCurrentDate = isSameDate(todaysDate, date);
|
||||
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||
|
||||
const renderDate = useCallback(
|
||||
(date: Date) => {
|
||||
const isCurrentDate = isSameDate(todaysDate, date);
|
||||
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||
return (
|
||||
<View style={{alignItems: "center"}}>
|
||||
<View style={appliedStyle}>
|
||||
<Text style={{color: isCurrentDate ? "white" : "black"}}>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[todaysDate, currentDateStyle, defaultStyle] // dependencies
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: "center" }}>
|
||||
<View style={appliedStyle}>
|
||||
<Text style={{ color: isCurrentDate ? "white" : "black" }}>
|
||||
{date.getDate()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[todaysDate, currentDateStyle, defaultStyle] // dependencies
|
||||
);
|
||||
return renderDate(date);
|
||||
};
|
||||
|
||||
return renderDate(date);
|
||||
};
|
||||
useEffect(() => {
|
||||
setOffsetMinutes(getTotalMinutes());
|
||||
}, [events, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
setOffsetMinutes(getTotalMinutes());
|
||||
}, [events, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
.then(() => {
|
||||
console.log('✅ Events refreshed successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('❌ Events refresh failed:', error);
|
||||
});
|
||||
}, [shouldRefresh, refetch])
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
<ActivityIndicator size="large" color="#0000ff"/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(enrichedEvents, filteredEvents)
|
||||
// console.log(enrichedEvents, filteredEvents)
|
||||
|
||||
return (
|
||||
<>
|
||||
return (
|
||||
<>
|
||||
{isSyncing && (
|
||||
<View style={styles.loadingContainer}>
|
||||
{isSyncing && <Text>Syncing...</Text>}
|
||||
@ -317,14 +302,14 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||
dayHeaderHighlightColor={"white"}
|
||||
showAdjacentMonths
|
||||
headerContainerStyle={mode !== "month" ? {
|
||||
overflow:"hidden",
|
||||
overflow: "hidden",
|
||||
} : {}}
|
||||
hourStyle={styles.hourStyle}
|
||||
onPressDateHeader={handlePressDayHeader}
|
||||
onPressDateHeader={handlePressDayHeader}
|
||||
ampm
|
||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||
/>
|
||||
<View style={{backgroundColor: 'white', height: 50, width: '100%'}} />
|
||||
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
|
||||
</>
|
||||
|
||||
);
|
||||
|
@ -11,7 +11,6 @@ import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView
|
||||
import { Dimensions, Platform, StyleSheet } from "react-native";
|
||||
|
||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||
import KeyboardManager from "react-native-keyboard-manager";
|
||||
import { useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||
|
||||
|
@ -14,9 +14,6 @@ import PenIcon from "@/assets/svgs/PenIcon";
|
||||
import BinIcon from "@/assets/svgs/BinIcon";
|
||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||
import RemindersIcon from "@/assets/svgs/RemindersIcon";
|
||||
import MenuIcon from "@/assets/svgs/MenuIcon";
|
||||
import { IFeedback, useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||
import FeedbackDialog from "./FeedbackDialog";
|
||||
|
@ -2,7 +2,6 @@ import {Dimensions, StyleSheet} from "react-native";
|
||||
import React from "react";
|
||||
import {Button, View,} from "react-native-ui-lib";
|
||||
import {useGroceryContext} from "@/contexts/GroceryContext";
|
||||
import {FontAwesome6} from "@expo/vector-icons";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
|
||||
const { width } = Dimensions.get("screen");
|
||||
|
@ -1,14 +1,25 @@
|
||||
import {FlatList, StyleSheet} from "react-native";
|
||||
import React from "react";
|
||||
import React, {useCallback} from "react";
|
||||
import {Card, Text, View} from "react-native-ui-lib";
|
||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||
import {useGetNotifications} from "@/hooks/firebase/useGetNotifications";
|
||||
import {Notification, useGetNotifications} from "@/hooks/firebase/useGetNotifications";
|
||||
import {formatDistanceToNow} from "date-fns";
|
||||
import {useRouter} from "expo-router";
|
||||
import {useSetAtom} from "jotai";
|
||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||
|
||||
const NotificationsPage = () => {
|
||||
const {data: notifications} = useGetNotifications()
|
||||
|
||||
console.log(notifications?.[0])
|
||||
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||
const setMode = useSetAtom(modeAtom);
|
||||
const {data: notifications} = useGetNotifications();
|
||||
const {push} = useRouter();
|
||||
const goToEventDay = useCallback((notification: Notification) => () => {
|
||||
if (notification?.date) {
|
||||
setSelectedDate(notification.date);
|
||||
setMode("day")
|
||||
}
|
||||
push({pathname: "/calendar"});
|
||||
}, [push, setSelectedDate]);
|
||||
|
||||
|
||||
return (
|
||||
@ -18,32 +29,56 @@ const NotificationsPage = () => {
|
||||
<HeaderTemplate
|
||||
message={"Welcome to your notifications!"}
|
||||
isWelcome={false}
|
||||
children={
|
||||
<Text
|
||||
style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
|
||||
>
|
||||
See your notifications here.
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Text style={styles.subtitle}>
|
||||
See your notifications here.
|
||||
</Text>
|
||||
</HeaderTemplate>
|
||||
</View>
|
||||
|
||||
<FlatList contentContainerStyle={{paddingBottom: 10, paddingHorizontal: 25}}
|
||||
data={notifications ?? []}
|
||||
renderItem={({item}) => <Card padding-20 gap-10 marginB-10>
|
||||
<Text text70>{item.content}</Text>
|
||||
<View row spread>
|
||||
<Text
|
||||
text90>{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}</Text>
|
||||
<Text text90>{item.timestamp.toLocaleDateString()}</Text>
|
||||
</View>
|
||||
</Card>}/>
|
||||
</View>
|
||||
<FlatList
|
||||
contentContainerStyle={styles.listContainer}
|
||||
data={notifications ?? []}
|
||||
renderItem={({item}) => (
|
||||
<Card
|
||||
padding-20
|
||||
marginB-10
|
||||
key={item.content}
|
||||
onPress={goToEventDay(item)}
|
||||
activeOpacity={0.6}
|
||||
enableShadow={false}
|
||||
style={styles.card}
|
||||
>
|
||||
<Text text70>{item.content}</Text>
|
||||
<View row spread marginT-10>
|
||||
<Text text90>
|
||||
{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}
|
||||
</Text>
|
||||
<Text text90>
|
||||
{item.timestamp.toLocaleDateString()}
|
||||
</Text>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: {
|
||||
paddingBottom: 10,
|
||||
paddingHorizontal: 25,
|
||||
},
|
||||
card: {
|
||||
width: '100%',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
subtitle: {
|
||||
fontFamily: "Manrope_400Regular",
|
||||
fontSize: 14,
|
||||
},
|
||||
searchField: {
|
||||
borderWidth: 0.7,
|
||||
borderColor: "#9b9b9b",
|
||||
@ -54,4 +89,4 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
export default NotificationsPage;
|
||||
export default NotificationsPage;
|
@ -1,91 +1,88 @@
|
||||
import { Image } from "react-native";
|
||||
import React, { useRef } from "react";
|
||||
import { View, Text, Button, TextField } from "react-native-ui-lib";
|
||||
import {Image, StyleSheet} from "react-native";
|
||||
import React, {useRef} from "react";
|
||||
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||
import Onboarding from "react-native-onboarding-swiper";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useSignUp } from "@/hooks/firebase/useSignUp";
|
||||
const OnboardingFlow = () => {
|
||||
const onboardingRef = useRef(null);
|
||||
const { mutateAsync: signUp } = useSignUp();
|
||||
|
||||
return (
|
||||
<Onboarding
|
||||
showPagination={false}
|
||||
ref={onboardingRef}
|
||||
containerStyles={{ backgroundColor: "#f9f8f7" }}
|
||||
imageContainerStyles={{
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
}}
|
||||
pages={[
|
||||
{
|
||||
backgroundColor: "#f9f8f7",
|
||||
image: (
|
||||
<Image
|
||||
source={require("../../../assets/images/splash-clock.png")}
|
||||
height={10}
|
||||
width={10}
|
||||
/>
|
||||
),
|
||||
title: <Text text30>Welcome to Cally</Text>,
|
||||
subtitle: (
|
||||
<View paddingB-250 marginH-20 spread>
|
||||
<Text text50R>Lightening Mental Loads, One Family at a Time</Text>
|
||||
<Button
|
||||
label="Continue"
|
||||
style={{ backgroundColor: "#fd1775" }}
|
||||
onPress={() => onboardingRef?.current?.goToPage(1, true)}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
{
|
||||
backgroundColor: "#f9f8f7",
|
||||
title: <Text>Get started with Cally</Text>,
|
||||
image: (
|
||||
<Image
|
||||
source={require("../../../assets/images/splash-clock.png")}
|
||||
height={10}
|
||||
width={10}
|
||||
/>
|
||||
),
|
||||
subtitle: (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: "auto",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<View marginH-30>
|
||||
{/*<TextField style={styles.textfield} placeholder="First name" />*/}
|
||||
{/*<TextField style={styles.textfield} placeholder="Last name" />*/}
|
||||
<TextField style={styles.textfield} placeholder="Email" />
|
||||
<TextField style={styles.textfield} placeholder="Password" />
|
||||
<Button
|
||||
label="Login"
|
||||
backgroundColor="#ea156c"
|
||||
onPress={() => {
|
||||
console.log("Onboarding Done");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const OnboardingFlow = () => {
|
||||
const onboardingRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Onboarding
|
||||
showPagination={false}
|
||||
ref={onboardingRef}
|
||||
containerStyles={{backgroundColor: "#f9f8f7"}}
|
||||
imageContainerStyles={{
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
}}
|
||||
pages={[
|
||||
{
|
||||
backgroundColor: "#f9f8f7",
|
||||
image: (
|
||||
<Image
|
||||
source={require("../../../assets/images/splash-clock.png")}
|
||||
height={10}
|
||||
width={10}
|
||||
/>
|
||||
),
|
||||
title: <Text text30>Welcome to Cally</Text>,
|
||||
subtitle: (
|
||||
<View paddingB-250 marginH-20 spread>
|
||||
<Text text50R>Lightening Mental Loads, One Family at a Time</Text>
|
||||
<Button
|
||||
label="Continue"
|
||||
style={{backgroundColor: "#fd1775"}}
|
||||
onPress={() => onboardingRef?.current?.goToPage(1, true)}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
{
|
||||
backgroundColor: "#f9f8f7",
|
||||
title: <Text>Get started with Cally</Text>,
|
||||
image: (
|
||||
<Image
|
||||
source={require("../../../assets/images/splash-clock.png")}
|
||||
height={10}
|
||||
width={10}
|
||||
/>
|
||||
),
|
||||
subtitle: (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: "auto",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<View marginH-30>
|
||||
{/*<TextField style={styles.textfield} placeholder="First name" />*/}
|
||||
{/*<TextField style={styles.textfield} placeholder="Last name" />*/}
|
||||
<TextField style={styles.textfield} placeholder="Email"/>
|
||||
<TextField style={styles.textfield} placeholder="Password"/>
|
||||
<Button
|
||||
label="Login"
|
||||
backgroundColor="#ea156c"
|
||||
onPress={() => {
|
||||
console.log("Onboarding Done");
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingFlow;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 10,
|
||||
padding: 30,
|
||||
height: 45,
|
||||
borderRadius: 50,
|
||||
},
|
||||
textfield: {
|
||||
backgroundColor: "white",
|
||||
marginVertical: 10,
|
||||
padding: 30,
|
||||
height: 45,
|
||||
borderRadius: 50,
|
||||
},
|
||||
});
|
||||
|
@ -2,7 +2,6 @@ import React, { useState } from "react";
|
||||
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
visible: boolean;
|
||||
|
@ -33,8 +33,6 @@ import KeyboardManager, {
|
||||
} from "react-native-keyboard-manager";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { useUploadProfilePicture } from "@/hooks/useUploadProfilePicture";
|
||||
import { ImagePickerAsset } from "expo-image-picker";
|
||||
import MenuDotsIcon from "@/assets/svgs/MenuDotsIcon";
|
||||
import UserOptions from "./UserOptions";
|
||||
|
||||
type MyGroupProps = {
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import QRCode from "react-native-qrcode-svg";
|
||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/components/panningViews/panningProvider";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { ProfileType } from "@/contexts/AuthContext";
|
||||
|
@ -1,68 +1,66 @@
|
||||
import { Dimensions, StyleSheet } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { Button, ButtonSize, Text, View } from "react-native-ui-lib";
|
||||
import { AntDesign } from "@expo/vector-icons";
|
||||
import LinearGradient from "react-native-linear-gradient";
|
||||
import {StyleSheet} from "react-native";
|
||||
import React, {useState} from "react";
|
||||
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||
import AddChoreDialog from "./AddChoreDialog";
|
||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||
|
||||
const AddChore = () => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<View
|
||||
row
|
||||
spread
|
||||
paddingH-20
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 15,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
marginB-30
|
||||
size={ButtonSize.large}
|
||||
style={styles.button}
|
||||
onPress={() => setIsVisible(!isVisible)}
|
||||
return (
|
||||
<View
|
||||
row
|
||||
spread
|
||||
paddingH-20
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 15,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
<Text
|
||||
white
|
||||
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 15 }}
|
||||
marginL-5
|
||||
>
|
||||
Create new to do
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
{isVisible && (
|
||||
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
marginB-30
|
||||
size={ButtonSize.large}
|
||||
style={styles.button}
|
||||
onPress={() => setIsVisible(!isVisible)}
|
||||
>
|
||||
<PlusIcon/>
|
||||
<Text
|
||||
white
|
||||
style={{fontFamily: "Manrope_600SemiBold", fontSize: 15}}
|
||||
marginL-5
|
||||
>
|
||||
Create new to do
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
{isVisible && (
|
||||
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible}/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddChore;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
gradient: {
|
||||
height: 150,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonContainer: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "rgb(253, 23, 117)",
|
||||
height: 53.26,
|
||||
borderRadius: 30,
|
||||
width: 335,
|
||||
},
|
||||
gradient: {
|
||||
height: 150,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonContainer: {
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "rgb(253, 23, 117)",
|
||||
height: 53.26,
|
||||
borderRadius: 30,
|
||||
width: 335,
|
||||
},
|
||||
});
|
||||
|
@ -1,44 +1,43 @@
|
||||
import { View, Text, Button } from "react-native-ui-lib";
|
||||
import {Text, View} from "react-native-ui-lib";
|
||||
import React from "react";
|
||||
import { Fontisto } from "@expo/vector-icons";
|
||||
import { ProgressBar } from "react-native-ui-lib/src/components/progressBar";
|
||||
import { useToDosContext } from "@/contexts/ToDosContext";
|
||||
import {ProgressBar} from "react-native-ui-lib/src/components/progressBar";
|
||||
import {useToDosContext} from "@/contexts/ToDosContext";
|
||||
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
||||
|
||||
const ProgressCard = ({children}: {children?: React.ReactNode}) => {
|
||||
const { maxPoints } = useToDosContext();
|
||||
return (
|
||||
<View
|
||||
backgroundColor="white"
|
||||
marginB-5
|
||||
padding-15
|
||||
style={{ borderRadius: 22 }}
|
||||
>
|
||||
<View row centerV>
|
||||
<FireworksOrangeIcon />
|
||||
<Text marginL-15 text70 style={{fontFamily: 'Manrope_600SemiBold', fontSize: 14}}>
|
||||
You have earned XX points this week!{" "}
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar
|
||||
progress={50}
|
||||
progressColor="#ea156c"
|
||||
style={{
|
||||
height: 21,
|
||||
backgroundColor: "#fcf2f6",
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
/>
|
||||
<View row spread>
|
||||
<Text style={{fontSize: 13, color: '#858585'}}>0</Text>
|
||||
<Text style={{fontSize: 13, color: '#858585'}}>{maxPoints}</Text>
|
||||
</View>
|
||||
<View centerV centerH>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
const ProgressCard = ({children}: { children?: React.ReactNode }) => {
|
||||
const {maxPoints} = useToDosContext();
|
||||
return (
|
||||
<View
|
||||
backgroundColor="white"
|
||||
marginB-5
|
||||
padding-15
|
||||
style={{borderRadius: 22}}
|
||||
>
|
||||
<View row centerV>
|
||||
<FireworksOrangeIcon/>
|
||||
<Text marginL-15 text70 style={{fontFamily: 'Manrope_600SemiBold', fontSize: 14}}>
|
||||
You have earned XX points this week!{" "}
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar
|
||||
progress={50}
|
||||
progressColor="#ea156c"
|
||||
style={{
|
||||
height: 21,
|
||||
backgroundColor: "#fcf2f6",
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
/>
|
||||
<View row spread>
|
||||
<Text style={{fontSize: 13, color: '#858585'}}>0</Text>
|
||||
<Text style={{fontSize: 13, color: '#858585'}}>{maxPoints}</Text>
|
||||
</View>
|
||||
<View centerV centerH>
|
||||
{children}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressCard;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { BarChart } from "react-native-gifted-charts";
|
||||
|
||||
const FamilyChart = () => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { BarChart } from "react-native-gifted-charts";
|
||||
|
||||
const UserChart = () => {
|
||||
|
@ -1,191 +1,181 @@
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ProgressBar,
|
||||
Button,
|
||||
ButtonSize,
|
||||
Modal,
|
||||
Dialog,
|
||||
TouchableOpacity,
|
||||
} from "react-native-ui-lib";
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import {Button, ButtonSize, Dialog, ProgressBar, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||
import React, {useState} from "react";
|
||||
import {StyleSheet} from "react-native";
|
||||
import UserChart from "./UserChart";
|
||||
import ProgressCard from "../ProgressCard";
|
||||
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { PanViewDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
||||
import {ScrollView} from "react-native-gesture-handler";
|
||||
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
||||
|
||||
const UserChoresProgress = ({
|
||||
setPageIndex,
|
||||
}: {
|
||||
setPageIndex: (value: number) => void;
|
||||
setPageIndex,
|
||||
}: {
|
||||
setPageIndex: (value: number) => void;
|
||||
}) => {
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
return (
|
||||
<View marginT-20 paddingB-20>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||
<View row marginT-4 marginB-10 centerV>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={14}
|
||||
color="#979797"
|
||||
style={{ paddingBottom: 3 }}
|
||||
/>
|
||||
<Text
|
||||
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
||||
color="#979797"
|
||||
>
|
||||
Return to To Do's
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
||||
Your To Do's Progress Report
|
||||
</Text>
|
||||
</View>
|
||||
<View row spread marginT-25 marginB-5>
|
||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
||||
Daily Goal
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressCard />
|
||||
<View row spread marginT-15 marginB-8>
|
||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
||||
Points Earned This Week
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card} paddingL-10>
|
||||
<UserChart />
|
||||
</View>
|
||||
<View row spread marginT-20 marginB-8 centerV>
|
||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
||||
Total Reward Points
|
||||
</Text>
|
||||
<Button
|
||||
size={ButtonSize.small}
|
||||
label="Spend my points"
|
||||
color="#50be0c"
|
||||
backgroundColor="#ebf2e4"
|
||||
onPress={() => setModalVisible(true)}
|
||||
labelStyle={{
|
||||
fontSize: 13,
|
||||
fontFamily: "Manrope_400Regular",
|
||||
}}
|
||||
iconSource={() => (
|
||||
<AntDesign
|
||||
name="gift"
|
||||
size={20}
|
||||
style={{ marginRight: 5 }}
|
||||
color="#50be0c"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<View row centerV>
|
||||
<FireworksOrangeIcon color="#8005eb" />
|
||||
<Text
|
||||
marginL-8
|
||||
text70
|
||||
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
return (
|
||||
<View marginT-20 paddingB-20>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
You have 1200 points saved!
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar
|
||||
progress={80}
|
||||
progressColor="#ff9900"
|
||||
style={{
|
||||
height: 21,
|
||||
backgroundColor: "#faeedb",
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
/>
|
||||
<View row spread>
|
||||
<Text style={{ fontSize: 13, color: "#858585" }}>0</Text>
|
||||
<Text style={{ fontSize: 13, color: "#858585" }}>5000</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||
<View row marginT-4 marginB-10 centerV>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={14}
|
||||
color="#979797"
|
||||
style={{paddingBottom: 3}}
|
||||
/>
|
||||
<Text
|
||||
style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
|
||||
color="#979797"
|
||||
>
|
||||
Return to To Do's
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={{fontFamily: "Manrope_700Bold", fontSize: 20}}>
|
||||
Your To Do's Progress Report
|
||||
</Text>
|
||||
</View>
|
||||
<View row spread marginT-25 marginB-5>
|
||||
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||
Daily Goal
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressCard/>
|
||||
<View row spread marginT-15 marginB-8>
|
||||
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||
Points Earned This Week
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.card} paddingL-10>
|
||||
<UserChart/>
|
||||
</View>
|
||||
<View row spread marginT-20 marginB-8 centerV>
|
||||
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||
Total Reward Points
|
||||
</Text>
|
||||
<Button
|
||||
size={ButtonSize.small}
|
||||
label="Spend my points"
|
||||
color="#50be0c"
|
||||
backgroundColor="#ebf2e4"
|
||||
onPress={() => setModalVisible(true)}
|
||||
labelStyle={{
|
||||
fontSize: 13,
|
||||
fontFamily: "Manrope_400Regular",
|
||||
}}
|
||||
iconSource={() => (
|
||||
<AntDesign
|
||||
name="gift"
|
||||
size={20}
|
||||
style={{marginRight: 5}}
|
||||
color="#50be0c"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.card}>
|
||||
<View row centerV>
|
||||
<FireworksOrangeIcon color="#8005eb"/>
|
||||
<Text
|
||||
marginL-8
|
||||
text70
|
||||
style={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
|
||||
>
|
||||
You have 1200 points saved!
|
||||
</Text>
|
||||
</View>
|
||||
<ProgressBar
|
||||
progress={80}
|
||||
progressColor="#ff9900"
|
||||
style={{
|
||||
height: 21,
|
||||
backgroundColor: "#faeedb",
|
||||
marginTop: 15,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
/>
|
||||
<View row spread>
|
||||
<Text style={{fontSize: 13, color: "#858585"}}>0</Text>
|
||||
<Text style={{fontSize: 13, color: "#858585"}}>5000</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Dialog
|
||||
visible={modalVisible}
|
||||
onDismiss={() => setModalVisible(false)}
|
||||
children={
|
||||
<View style={styles.card} paddingH-35 paddingT-35>
|
||||
<Text text60 center marginB-35 style={{fontFamily: 'Manrope_600SemiBold'}}>
|
||||
How would you like to spend your points?
|
||||
</Text>
|
||||
<Button
|
||||
label="Skip a Chore Cor a Day - 150 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#05a8b6"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Extra Screen Time - 100 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#ea156c"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Movie Night - 50 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#7305d4"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Ice Cream Treat - 25 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#e28800"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<Text text70 marginT-20 center color="#999999" style={{fontFamily: 'Manrope_500Medium'}}>
|
||||
Go back to my to dos
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
<Dialog
|
||||
visible={modalVisible}
|
||||
onDismiss={() => setModalVisible(false)}
|
||||
children={
|
||||
<View style={styles.card} paddingH-35 paddingT-35>
|
||||
<Text text60 center marginB-35 style={{fontFamily: 'Manrope_600SemiBold'}}>
|
||||
How would you like to spend your points?
|
||||
</Text>
|
||||
<Button
|
||||
label="Skip a Chore Cor a Day - 150 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#05a8b6"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Extra Screen Time - 100 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#ea156c"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Movie Night - 50 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#7305d4"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<Button
|
||||
label="Ice Cream Treat - 25 pts"
|
||||
text70
|
||||
marginB-15
|
||||
backgroundColor="#e28800"
|
||||
size={ButtonSize.large}
|
||||
labelStyle={styles.bigButtonText}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||
<Text text70 marginT-20 center color="#999999" style={{fontFamily: 'Manrope_500Medium'}} >
|
||||
Go back to my to dos
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
);
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
pfpSmall: {
|
||||
width: 30,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
pfpBig: {
|
||||
width: 50,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 50,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
},
|
||||
bigButtonText:{
|
||||
fontFamily: 'Manrope_400Regular'
|
||||
}
|
||||
pfpSmall: {
|
||||
width: 30,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 2,
|
||||
},
|
||||
pfpBig: {
|
||||
width: 50,
|
||||
aspectRatio: 1,
|
||||
borderRadius: 50,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: "white",
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
},
|
||||
bigButtonText: {
|
||||
fontFamily: 'Manrope_400Regular'
|
||||
}
|
||||
});
|
||||
|
||||
export default UserChoresProgress;
|
||||
|
@ -2,9 +2,7 @@ import { useCreateNote } from "@/hooks/firebase/useCreateNote";
|
||||
import { useDeleteNote } from "@/hooks/firebase/useDeleteNote";
|
||||
import { useGetNotes } from "@/hooks/firebase/useGetNotes";
|
||||
import { useUpdateNote } from "@/hooks/firebase/useUpdateNote";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { create } from "react-test-renderer";
|
||||
|
||||
export interface IBrainDump {
|
||||
id: number;
|
||||
|
@ -1,87 +1,84 @@
|
||||
import { useCreateFeedback } from "@/hooks/firebase/useCreateFeedback";
|
||||
import { useDeleteFeedback } from "@/hooks/firebase/useDeleteFeedback";
|
||||
import { useGetFeedbacks } from "@/hooks/firebase/useGetFeedbacks";
|
||||
import { useUpdateFeedback } from "@/hooks/firebase/useUpdateFeedback";
|
||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import {useCreateFeedback} from "@/hooks/firebase/useCreateFeedback";
|
||||
import {useDeleteFeedback} from "@/hooks/firebase/useDeleteFeedback";
|
||||
import {useGetFeedbacks} from "@/hooks/firebase/useGetFeedbacks";
|
||||
import {useUpdateFeedback} from "@/hooks/firebase/useUpdateFeedback";
|
||||
import {createContext, useContext, useState} from "react";
|
||||
|
||||
export interface IFeedback {
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
id: number;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface IFeedbackContext {
|
||||
feedbacks: IFeedback[] | undefined;
|
||||
isAddingFeedback: boolean;
|
||||
setIsAddingFeedback: (value: boolean) => void;
|
||||
addFeedback: (BrainDump: IFeedback) => void;
|
||||
updateFeedback: (id: number, changes: Partial<IFeedback>) => void;
|
||||
deleteFeedback: (id: number) => void;
|
||||
feedbacks: IFeedback[] | undefined;
|
||||
isAddingFeedback: boolean;
|
||||
setIsAddingFeedback: (value: boolean) => void;
|
||||
addFeedback: (BrainDump: IFeedback) => void;
|
||||
updateFeedback: (id: number, changes: Partial<IFeedback>) => void;
|
||||
deleteFeedback: (id: number) => void;
|
||||
}
|
||||
|
||||
const FeedbackContext = createContext<IFeedbackContext | undefined>(undefined);
|
||||
|
||||
export const FeedbackProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const {
|
||||
mutateAsync: createFeedback,
|
||||
isLoading: isAdding,
|
||||
isError,
|
||||
} = useCreateFeedback();
|
||||
const { data: feedbacks } = useGetFeedbacks();
|
||||
const { mutate: deleteFeedbackMutate } = useDeleteFeedback();
|
||||
const { mutate: updateFeedbackMutate } = useUpdateFeedback();
|
||||
children,
|
||||
}) => {
|
||||
const {
|
||||
mutateAsync: createFeedback,
|
||||
} = useCreateFeedback();
|
||||
const {data: feedbacks} = useGetFeedbacks();
|
||||
const {mutate: deleteFeedbackMutate} = useDeleteFeedback();
|
||||
const {mutate: updateFeedbackMutate} = useUpdateFeedback();
|
||||
|
||||
const [isAddingFeedback, setIsAddingFeedback] = useState<boolean>(false);
|
||||
const [isAddingFeedback, setIsAddingFeedback] = useState<boolean>(false);
|
||||
|
||||
const addFeedback = (Feedback: IFeedback) => {
|
||||
createFeedback({ title: Feedback.title, text: Feedback.text });
|
||||
};
|
||||
const addFeedback = (Feedback: IFeedback) => {
|
||||
createFeedback({title: Feedback.title, text: Feedback.text});
|
||||
};
|
||||
|
||||
const updateFeedback = (id: number, changes: Partial<IFeedback>) => {
|
||||
updateFeedbackMutate(
|
||||
{
|
||||
id: id,
|
||||
changes: changes,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log("Feedback updated successfully", data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update feedback:", error);
|
||||
},
|
||||
}
|
||||
const updateFeedback = (id: number, changes: Partial<IFeedback>) => {
|
||||
updateFeedbackMutate(
|
||||
{
|
||||
id: id,
|
||||
changes: changes,
|
||||
},
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log("Feedback updated successfully", data);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update feedback:", error);
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const deleteFeedback = (id: number) => {
|
||||
deleteFeedbackMutate(id.toString(), {
|
||||
onSuccess: () => {
|
||||
console.log("Feedback deleted successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to delete feedback:", error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FeedbackContext.Provider
|
||||
value={{
|
||||
feedbacks,
|
||||
isAddingFeedback,
|
||||
setIsAddingFeedback,
|
||||
addFeedback,
|
||||
updateFeedback,
|
||||
deleteFeedback,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FeedbackContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const deleteFeedback = (id: number) => {
|
||||
deleteFeedbackMutate(id.toString(), {
|
||||
onSuccess: () => {
|
||||
console.log("Feedback deleted successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to delete feedback:", error);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FeedbackContext.Provider
|
||||
value={{
|
||||
feedbacks,
|
||||
isAddingFeedback,
|
||||
setIsAddingFeedback,
|
||||
addFeedback,
|
||||
updateFeedback,
|
||||
deleteFeedback,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FeedbackContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFeedbackContext = () => useContext(FeedbackContext)!;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { View, Text } from "react-native";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
export interface IReminder {
|
||||
id: number;
|
||||
|
@ -18,23 +18,190 @@ const GOOGLE_CALENDAR_ID = "primary";
|
||||
const CHANNEL_ID = "cally-family-calendar";
|
||||
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
||||
|
||||
async function getPushTokensForFamily(familyId, excludeUserId = null) {
|
||||
const usersRef = db.collection('Profiles');
|
||||
const snapshot = await usersRef.where('familyId', '==', familyId).get();
|
||||
let pushTokens = [];
|
||||
|
||||
snapshot.forEach(doc => {
|
||||
const data = doc.data();
|
||||
if ((!excludeUserId || data.uid !== excludeUserId) && data.pushToken) {
|
||||
pushTokens.push(data.pushToken);
|
||||
}
|
||||
});
|
||||
|
||||
return pushTokens;
|
||||
}
|
||||
|
||||
exports.sendOverviews = functions.pubsub
|
||||
.schedule('0 20 * * *')
|
||||
.onRun(async (context) => {
|
||||
const familiesSnapshot = await admin.firestore().collection('Families').get();
|
||||
|
||||
for (const familyDoc of familiesSnapshot.docs) {
|
||||
const familyId = familyDoc.id;
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const overviewTime = familySettings.overviewTime || '20:00';
|
||||
|
||||
const [hours, minutes] = overviewTime.split(':');
|
||||
const now = new Date();
|
||||
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 0, 0);
|
||||
|
||||
const tomorrowEnd = new Date(tomorrow);
|
||||
tomorrowEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
const weekEnd = new Date(tomorrow);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
weekEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
const [tomorrowEvents, weekEvents] = await Promise.all([
|
||||
admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>=', tomorrow)
|
||||
.where('startDate', '<=', tomorrowEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get(),
|
||||
|
||||
admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>', tomorrowEnd)
|
||||
.where('startDate', '<=', weekEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get()
|
||||
]);
|
||||
|
||||
if (tomorrowEvents.empty && weekEvents.empty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let notificationBody = '';
|
||||
|
||||
if (!tomorrowEvents.empty) {
|
||||
notificationBody += 'Tomorrow: ';
|
||||
const tomorrowTitles = tomorrowEvents.docs.map(doc => doc.data().title);
|
||||
notificationBody += tomorrowTitles.join(', ');
|
||||
if (tomorrowEvents.size === 3) notificationBody += ' and more...';
|
||||
}
|
||||
|
||||
if (!weekEvents.empty) {
|
||||
if (notificationBody) notificationBody += '\n\n';
|
||||
notificationBody += 'This week: ';
|
||||
const weekTitles = weekEvents.docs.map(doc => doc.data().title);
|
||||
notificationBody += weekTitles.join(', ');
|
||||
if (weekEvents.size === 3) notificationBody += ' and more...';
|
||||
}
|
||||
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Family Calendar Overview",
|
||||
body: notificationBody,
|
||||
data: {
|
||||
type: 'calendar_overview',
|
||||
date: tomorrow.toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await storeNotification({
|
||||
type: 'calendar_overview',
|
||||
familyId,
|
||||
content: notificationBody,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending overview for family ${familyId}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.sendWeeklyOverview = functions.pubsub
|
||||
.schedule('0 20 * * 0')
|
||||
.onRun(async (context) => {
|
||||
const familiesSnapshot = await admin.firestore().collection('Families').get();
|
||||
|
||||
for (const familyDoc of familiesSnapshot.docs) {
|
||||
const familyId = familyDoc.id;
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const overviewTime = familySettings.weeklyOverviewTime || '20:00';
|
||||
|
||||
const [hours, minutes] = overviewTime.split(':');
|
||||
const now = new Date();
|
||||
if (now.getHours() !== parseInt(hours) || now.getMinutes() !== parseInt(minutes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const weekStart = new Date();
|
||||
weekStart.setDate(weekStart.getDate() + 1);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekEnd.getDate() + 7);
|
||||
|
||||
const weekEvents = await admin.firestore()
|
||||
.collection('Events')
|
||||
.where('familyId', '==', familyId)
|
||||
.where('startDate', '>=', weekStart)
|
||||
.where('startDate', '<=', weekEnd)
|
||||
.orderBy('startDate')
|
||||
.limit(3)
|
||||
.get();
|
||||
|
||||
if (weekEvents.empty) continue;
|
||||
|
||||
const eventTitles = weekEvents.docs.map(doc => doc.data().title);
|
||||
const notificationBody = `Next week: ${eventTitles.join(', ')}${weekEvents.size === 3 ? ' and more...' : ''}`;
|
||||
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Weekly Calendar Overview",
|
||||
body: notificationBody,
|
||||
data: {
|
||||
type: 'weekly_overview',
|
||||
weekStart: weekStart.toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await storeNotification({
|
||||
type: 'weekly_overview',
|
||||
familyId,
|
||||
content: notificationBody,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error sending weekly overview for family ${familyId}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.sendNotificationOnEventCreation = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onCreate(async (snapshot, context) => {
|
||||
const eventData = snapshot.data();
|
||||
const { familyId, creatorId, email, title } = eventData;
|
||||
|
||||
if (!!eventData?.externalOrigin) {
|
||||
console.log('Externally synced event, ignoring.')
|
||||
return;
|
||||
}
|
||||
const {familyId, creatorId, email, title, externalOrigin} = eventData;
|
||||
|
||||
if (!familyId || !creatorId) {
|
||||
console.error('Missing familyId or creatorId in event data');
|
||||
return;
|
||||
}
|
||||
|
||||
let pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
|
||||
// Get push tokens - exclude creator for manual events, include everyone for synced events
|
||||
let pushTokens = await getPushTokensForFamily(
|
||||
familyId,
|
||||
externalOrigin ? null : creatorId // Only exclude creator for manual events
|
||||
);
|
||||
|
||||
if (!pushTokens.length) {
|
||||
console.log('No push tokens available for the event.');
|
||||
@ -48,9 +215,16 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
notificationInProgress = true;
|
||||
|
||||
notificationTimeout = setTimeout(async () => {
|
||||
const eventMessage = eventCount === 1
|
||||
? `An event "${title}" has been added. Check it out!`
|
||||
: `${eventCount} new events have been added.`;
|
||||
let eventMessage;
|
||||
if (externalOrigin) {
|
||||
eventMessage = eventCount === 1
|
||||
? `Calendar sync completed: "${title}" has been added.`
|
||||
: `Calendar sync completed: ${eventCount} new events have been added.`;
|
||||
} else {
|
||||
eventMessage = eventCount === 1
|
||||
? `New event "${title}" has been added to the family calendar.`
|
||||
: `${eventCount} new events have been added to the family calendar.`;
|
||||
}
|
||||
|
||||
let messages = pushTokens.map(pushToken => {
|
||||
if (!Expo.isExpoPushToken(pushToken)) {
|
||||
@ -61,9 +235,12 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
return {
|
||||
to: pushToken,
|
||||
sound: 'default',
|
||||
title: 'New Events Added!',
|
||||
title: externalOrigin ? 'Calendar Sync Complete' : 'New Family Calendar Events',
|
||||
body: eventMessage,
|
||||
data: { eventId: context.params.eventId },
|
||||
data: {
|
||||
eventId: context.params.eventId,
|
||||
type: externalOrigin ? 'sync' : 'manual'
|
||||
},
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
@ -90,13 +267,15 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
}
|
||||
}
|
||||
|
||||
// Save the notification in Firestore for record-keeping
|
||||
// Save the notification in Firestore
|
||||
const notificationData = {
|
||||
creatorId,
|
||||
familyId,
|
||||
content: eventMessage,
|
||||
eventId: context.params.eventId,
|
||||
type: externalOrigin ? 'sync' : 'manual',
|
||||
timestamp: Timestamp.now(),
|
||||
date: eventData.startDate
|
||||
};
|
||||
|
||||
try {
|
||||
@ -106,15 +285,159 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
||||
console.error("Error saving notification to Firestore:", error);
|
||||
}
|
||||
|
||||
// Reset state variables after notifications are sent
|
||||
// Reset state variables
|
||||
eventCount = 0;
|
||||
pushTokens = [];
|
||||
notificationInProgress = false;
|
||||
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
exports.onEventUpdate = functions.firestore
|
||||
.document('Events/{eventId}')
|
||||
.onUpdate(async (change, context) => {
|
||||
const beforeData = change.before.data();
|
||||
const afterData = change.after.data();
|
||||
const {familyId, title, lastModifiedBy} = afterData;
|
||||
|
||||
// Skip if no meaningful changes
|
||||
if (JSON.stringify(beforeData) === JSON.stringify(afterData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get push tokens excluding the user who made the change
|
||||
const pushTokens = await getPushTokensForFamily(familyId, lastModifiedBy);
|
||||
|
||||
const message = `Event "${title}" has been updated`;
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Event Updated",
|
||||
body: message,
|
||||
data: {
|
||||
type: 'event_update',
|
||||
eventId: context.params.eventId
|
||||
}
|
||||
});
|
||||
|
||||
// Store notification in Firestore
|
||||
await storeNotification({
|
||||
type: 'event_update',
|
||||
familyId,
|
||||
content: message,
|
||||
eventId: context.params.eventId,
|
||||
excludedUser: lastModifiedBy,
|
||||
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||
date: eventData.startDate
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending event update notification:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Upcoming Event Reminders
|
||||
exports.checkUpcomingEvents = functions.pubsub
|
||||
.schedule('every 5 minutes')
|
||||
.onRun(async (context) => {
|
||||
const now = admin.firestore.Timestamp.now();
|
||||
const eventsSnapshot = await admin.firestore().collection('Events').get();
|
||||
|
||||
for (const doc of eventsSnapshot.docs) {
|
||||
const event = doc.data();
|
||||
const {startDate, familyId, title, allDay, creatorId} = event;
|
||||
|
||||
if (startDate.toDate() < now.toDate()) continue;
|
||||
|
||||
try {
|
||||
const familyDoc = await admin.firestore().collection('Families').doc(familyId).get();
|
||||
const familySettings = familyDoc.data()?.settings || {};
|
||||
const reminderTime = familySettings.defaultReminderTime || 15; // minutes
|
||||
|
||||
const eventTime = startDate.toDate();
|
||||
const reminderThreshold = new Date(now.toDate().getTime() + (reminderTime * 60 * 1000));
|
||||
|
||||
// For all-day events, send reminder the evening before
|
||||
if (allDay) {
|
||||
const eveningBefore = new Date(eventTime);
|
||||
eveningBefore.setDate(eveningBefore.getDate() - 1);
|
||||
eveningBefore.setHours(20, 0, 0, 0);
|
||||
|
||||
if (now.toDate() >= eveningBefore && !event.eveningReminderSent) {
|
||||
// Get all family members' tokens (including creator for reminders)
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Tomorrow's All-Day Event",
|
||||
body: `Tomorrow: ${title}`,
|
||||
data: {
|
||||
type: 'event_reminder',
|
||||
eventId: doc.id
|
||||
}
|
||||
});
|
||||
|
||||
await doc.ref.update({eveningReminderSent: true});
|
||||
}
|
||||
}
|
||||
// For regular events, check if within reminder threshold
|
||||
else if (eventTime <= reminderThreshold && !event.reminderSent) {
|
||||
// Include creator for reminders
|
||||
const pushTokens = await getPushTokensForFamily(familyId);
|
||||
await sendNotifications(pushTokens, {
|
||||
title: "Upcoming Event",
|
||||
body: `In ${reminderTime} minutes: ${title}`,
|
||||
data: {
|
||||
type: 'event_reminder',
|
||||
eventId: doc.id
|
||||
}
|
||||
});
|
||||
|
||||
await doc.ref.update({reminderSent: true});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing reminder for event ${doc.id}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function storeNotification(notificationData) {
|
||||
try {
|
||||
await admin.firestore()
|
||||
.collection('Notifications')
|
||||
.add(notificationData);
|
||||
} catch (error) {
|
||||
console.error('Error storing notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendNotifications(pushTokens, notification) {
|
||||
if (!pushTokens.length) return;
|
||||
|
||||
const messages = pushTokens
|
||||
.filter(token => Expo.isExpoPushToken(token))
|
||||
.map(pushToken => ({
|
||||
to: pushToken,
|
||||
sound: 'default',
|
||||
priority: 'high',
|
||||
...notification
|
||||
}));
|
||||
|
||||
const chunks = expo.chunkPushNotifications(messages);
|
||||
|
||||
for (let chunk of chunks) {
|
||||
try {
|
||||
const tickets = await expo.sendPushNotificationsAsync(chunk);
|
||||
for (let ticket of tickets) {
|
||||
if (ticket.status === "error") {
|
||||
if (ticket.details?.error === "DeviceNotRegistered") {
|
||||
await removeInvalidPushToken(ticket.to);
|
||||
}
|
||||
console.error('Push notification error:', ticket.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending notifications:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.createSubUser = onRequest(async (request, response) => {
|
||||
const authHeader = request.get('Authorization');
|
||||
|
||||
@ -209,7 +532,7 @@ exports.removeSubUser = onRequest(async (request, response) => {
|
||||
|
||||
logger.info("Processing user removal", {requestBody: request.body.data});
|
||||
|
||||
const { userId, familyId } = request.body.data;
|
||||
const {userId, familyId} = request.body.data;
|
||||
|
||||
if (!userId || !familyId) {
|
||||
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
|
||||
@ -397,7 +720,22 @@ async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
||||
}
|
||||
|
||||
async function removeInvalidPushToken(pushToken) {
|
||||
// TODO
|
||||
try {
|
||||
const profilesRef = db.collection('Profiles');
|
||||
const snapshot = await profilesRef.where('pushToken', '==', pushToken).get();
|
||||
|
||||
const batch = db.batch();
|
||||
snapshot.forEach(doc => {
|
||||
batch.update(doc.ref, {
|
||||
pushToken: admin.firestore.FieldValue.delete()
|
||||
});
|
||||
});
|
||||
|
||||
await batch.commit();
|
||||
console.log(`Removed invalid push token: ${pushToken}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing invalid push token:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = require("node-fetch");
|
||||
@ -667,7 +1005,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId }) {
|
||||
async function fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||
const baseDate = new Date();
|
||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||
@ -700,7 +1038,7 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
||||
token = refreshedToken;
|
||||
|
||||
if (token) {
|
||||
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
|
||||
return fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId});
|
||||
} else {
|
||||
console.error(`Failed to refresh token for user: ${email}`);
|
||||
await clearToken(email);
|
||||
@ -717,8 +1055,12 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
||||
const googleEvent = {
|
||||
id: item.id,
|
||||
title: item.summary || "",
|
||||
startDate: item.start?.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date),
|
||||
endDate: item.end?.dateTime ? new Date(item.end.dateTime) : new Date(item.end.date),
|
||||
startDate: item.start?.dateTime
|
||||
? new Date(item.start.dateTime)
|
||||
: new Date(item.start.date + 'T00:00:00'),
|
||||
endDate: item.end?.dateTime
|
||||
? new Date(item.end.dateTime)
|
||||
: new Date(new Date(item.end.date + 'T00:00:00').setDate(new Date(item.end.date).getDate() - 1)),
|
||||
allDay: !item.start?.dateTime,
|
||||
familyId,
|
||||
email,
|
||||
@ -743,12 +1085,12 @@ async function saveEventsToFirestore(events) {
|
||||
const batch = db.batch();
|
||||
events.forEach((event) => {
|
||||
const eventRef = db.collection("Events").doc(event.id);
|
||||
batch.set(eventRef, event, { merge: true });
|
||||
batch.set(eventRef, event, {merge: true});
|
||||
});
|
||||
await batch.commit();
|
||||
}
|
||||
|
||||
async function calendarSync({ userId, email, token, refreshToken, familyId }) {
|
||||
async function calendarSync({userId, email, token, refreshToken, familyId}) {
|
||||
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
|
||||
try {
|
||||
await fetchAndSaveGoogleEvents({
|
||||
@ -787,7 +1129,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { googleAccounts } = userData;
|
||||
const {googleAccounts} = userData;
|
||||
const email = Object.keys(googleAccounts || {})[0];
|
||||
const accountData = googleAccounts[email] || {};
|
||||
const token = accountData.accessToken;
|
||||
@ -795,7 +1137,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
const familyId = userData.familyId;
|
||||
|
||||
console.log("Starting calendar sync...");
|
||||
await calendarSync({ userId, email, token, refreshToken, familyId });
|
||||
await calendarSync({userId, email, token, refreshToken, familyId});
|
||||
console.log("Calendar sync completed.");
|
||||
|
||||
res.status(200).send("Sync notification sent.");
|
||||
@ -803,4 +1145,202 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
||||
console.error(`Error in sendSyncNotification for user ${userId}:`, error.message);
|
||||
res.status(500).send("Failed to send sync notification.");
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshMicrosoftToken(refreshToken) {
|
||||
try {
|
||||
console.log("Refreshing Microsoft token...");
|
||||
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
||||
scope: "openid profile email offline_access Calendars.ReadWrite User.Read",
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error("Error refreshing Microsoft token:", errorData);
|
||||
throw new Error(`Failed to refresh Microsoft token: ${errorData.error || response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Microsoft token refreshed successfully");
|
||||
|
||||
return {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token || refreshToken
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error refreshing Microsoft token:", error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndSaveMicrosoftEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||
const baseDate = new Date();
|
||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||
|
||||
try {
|
||||
console.log(`Fetching Microsoft calendar events for user: ${email}`);
|
||||
|
||||
const url = `https://graph.microsoft.com/v1.0/me/calendar/events`;
|
||||
const queryParams = new URLSearchParams({
|
||||
$select: 'subject,start,end,id',
|
||||
$filter: `start/dateTime ge '${timeMin}' and end/dateTime le '${timeMax}'`
|
||||
});
|
||||
|
||||
const response = await fetch(`${url}?${queryParams}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401 && refreshToken) {
|
||||
console.log(`Token expired for user: ${email}, attempting to refresh`);
|
||||
const {accessToken: newToken} = await refreshMicrosoftToken(refreshToken);
|
||||
if (newToken) {
|
||||
return fetchAndSaveMicrosoftEvents({
|
||||
token: newToken,
|
||||
refreshToken,
|
||||
email,
|
||||
familyId,
|
||||
creatorId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||
}
|
||||
|
||||
const events = data.value.map(item => ({
|
||||
id: item.id,
|
||||
title: item.subject || "",
|
||||
startDate: new Date(item.start.dateTime + 'Z'),
|
||||
endDate: new Date(item.end.dateTime + 'Z'),
|
||||
allDay: false, // Microsoft Graph API handles all-day events differently
|
||||
familyId,
|
||||
email,
|
||||
creatorId,
|
||||
externalOrigin: "microsoft"
|
||||
}));
|
||||
|
||||
console.log(`Saving Microsoft calendar events to Firestore for user: ${email}`);
|
||||
await saveEventsToFirestore(events);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Microsoft Calendar events for ${email}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribeMicrosoftCalendar(accessToken, userId) {
|
||||
try {
|
||||
console.log(`Setting up Microsoft calendar subscription for user ${userId}`);
|
||||
|
||||
const subscription = {
|
||||
changeType: "created,updated,deleted",
|
||||
notificationUrl: `https://us-central1-cally-family-calendar.cloudfunctions.net/microsoftCalendarWebhook?userId=${userId}`,
|
||||
resource: "/me/calendar/events",
|
||||
expirationDateTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days
|
||||
clientState: userId
|
||||
};
|
||||
|
||||
const response = await fetch('https://graph.microsoft.com/v1.0/subscriptions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create subscription: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const subscriptionData = await response.json();
|
||||
|
||||
// Store subscription details in Firestore
|
||||
await admin.firestore().collection('MicrosoftSubscriptions').doc(userId).set({
|
||||
subscriptionId: subscriptionData.id,
|
||||
expirationDateTime: subscriptionData.expirationDateTime,
|
||||
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
||||
});
|
||||
|
||||
console.log(`Microsoft calendar subscription created for user ${userId}`);
|
||||
return subscriptionData;
|
||||
} catch (error) {
|
||||
console.error(`Error creating Microsoft calendar subscription for user ${userId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.microsoftCalendarWebhook = functions.https.onRequest(async (req, res) => {
|
||||
const userId = req.query.userId;
|
||||
console.log(`Received Microsoft calendar webhook for user ${userId}`, req.body);
|
||||
|
||||
try {
|
||||
const userDoc = await admin.firestore().collection("Profiles").doc(userId).get();
|
||||
const userData = userDoc.data();
|
||||
|
||||
if (!userData?.microsoftAccounts) {
|
||||
console.log(`No Microsoft account found for user ${userId}`);
|
||||
return res.status(200).send();
|
||||
}
|
||||
|
||||
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||
const token = userData.microsoftAccounts[email];
|
||||
|
||||
await fetchAndSaveMicrosoftEvents({
|
||||
token,
|
||||
email,
|
||||
familyId: userData.familyId,
|
||||
creatorId: userId
|
||||
});
|
||||
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
console.error(`Error processing Microsoft calendar webhook for user ${userId}:`, error);
|
||||
res.status(500).send();
|
||||
}
|
||||
});
|
||||
exports.renewMicrosoftSubscriptions = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
|
||||
console.log('Starting Microsoft subscription renewal process');
|
||||
|
||||
try {
|
||||
const subscriptionsSnapshot = await admin.firestore()
|
||||
.collection('MicrosoftSubscriptions')
|
||||
.get();
|
||||
|
||||
for (const doc of subscriptionsSnapshot.docs) {
|
||||
const userId = doc.id;
|
||||
const userDoc = await admin.firestore().collection('Profiles').doc(userId).get();
|
||||
const userData = userDoc.data();
|
||||
|
||||
if (userData?.microsoftAccounts) {
|
||||
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||
const token = userData.microsoftAccounts[email];
|
||||
|
||||
try {
|
||||
await subscribeMicrosoftCalendar(token, userId);
|
||||
console.log(`Renewed Microsoft subscription for user ${userId}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to renew Microsoft subscription for user ${userId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in Microsoft subscription renewal process:', error);
|
||||
}
|
||||
});
|
@ -1,13 +1,13 @@
|
||||
import { useQuery } from "react-query";
|
||||
import {useQuery} from "react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
||||
import { colorMap } from "@/constants/colorMap";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import {useAtomValue} from "jotai";
|
||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||
import {colorMap} from "@/constants/colorMap";
|
||||
import {uuidv4} from "@firebase/util";
|
||||
|
||||
export const useGetEvents = () => {
|
||||
const { user, profileData } = useAuthContext();
|
||||
const {user, profileData} = useAuthContext();
|
||||
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
||||
|
||||
return useQuery({
|
||||
@ -19,62 +19,61 @@ export const useGetEvents = () => {
|
||||
|
||||
let allEvents = [];
|
||||
|
||||
// If family view is active, include family, creator, and attendee events
|
||||
if (isFamilyView) {
|
||||
const familyQuery = db.collection("Events").where("familyId", "==", familyId);
|
||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
||||
// Get public family events
|
||||
const publicFamilyEvents = await db.collection("Events")
|
||||
.where("familyId", "==", familyId)
|
||||
.where("private", "==", false)
|
||||
.get();
|
||||
|
||||
const [familySnapshot, attendeeSnapshot] = await Promise.all([
|
||||
familyQuery.get(),
|
||||
attendeeQuery.get(),
|
||||
]);
|
||||
// Get private events where user is creator
|
||||
const privateCreatorEvents = await db.collection("Events")
|
||||
.where("familyId", "==", familyId)
|
||||
.where("private", "==", true)
|
||||
.where("creatorId", "==", userId)
|
||||
.get();
|
||||
|
||||
// Collect all events
|
||||
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
||||
// Get private events where user is attendee
|
||||
const privateAttendeeEvents = await db.collection("Events")
|
||||
.where("private", "==", true)
|
||||
.where("attendees", "array-contains", userId)
|
||||
.get();
|
||||
|
||||
// console.log("Family events not in creator query: ", familyEvents.filter(event => !creatorEvents.some(creatorEvent => creatorEvent.id === event.id)));
|
||||
|
||||
|
||||
allEvents = [...familyEvents, ...attendeeEvents];
|
||||
allEvents = [
|
||||
...publicFamilyEvents.docs.map(doc => doc.data()),
|
||||
...privateCreatorEvents.docs.map(doc => doc.data()),
|
||||
...privateAttendeeEvents.docs.map(doc => doc.data())
|
||||
];
|
||||
} else {
|
||||
// Only include creator and attendee events when family view is off
|
||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
||||
|
||||
const [creatorSnapshot, attendeeSnapshot] = await Promise.all([
|
||||
creatorQuery.get(),
|
||||
attendeeQuery.get(),
|
||||
// Personal view: Only show events where user is creator or attendee
|
||||
const [creatorEvents, attendeeEvents] = await Promise.all([
|
||||
db.collection("Events")
|
||||
.where("creatorId", "==", userId)
|
||||
.get(),
|
||||
db.collection("Events")
|
||||
.where("attendees", "array-contains", userId)
|
||||
.get()
|
||||
]);
|
||||
|
||||
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
|
||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
||||
|
||||
allEvents = [...creatorEvents, ...attendeeEvents];
|
||||
allEvents = [
|
||||
...creatorEvents.docs.map(doc => doc.data()),
|
||||
...attendeeEvents.docs.map(doc => doc.data())
|
||||
];
|
||||
}
|
||||
|
||||
// Use a Map to ensure uniqueness only for events with IDs
|
||||
// Ensure uniqueness
|
||||
const uniqueEventsMap = new Map();
|
||||
allEvents.forEach(event => {
|
||||
if (event.id) {
|
||||
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
|
||||
uniqueEventsMap.set(event.id, event);
|
||||
} else {
|
||||
uniqueEventsMap.set(uuidv4(), event); // Generate a temp key for events without ID
|
||||
uniqueEventsMap.set(uuidv4(), event);
|
||||
}
|
||||
});
|
||||
const uniqueEvents = Array.from(uniqueEventsMap.values());
|
||||
|
||||
// Filter out private events unless the user is the creator
|
||||
const filteredEvents = uniqueEvents.filter(event => {
|
||||
if (event.private) {
|
||||
return event.creatorId === userId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Attach event colors and return the final list of events
|
||||
// Map events with creator colors
|
||||
return await Promise.all(
|
||||
filteredEvents.map(async (event) => {
|
||||
Array.from(uniqueEventsMap.values()).map(async (event) => {
|
||||
const profileSnapshot = await db
|
||||
.collection("Profiles")
|
||||
.doc(event.creatorId)
|
||||
@ -85,10 +84,12 @@ export const useGetEvents = () => {
|
||||
|
||||
return {
|
||||
...event,
|
||||
id: event.id || Math.random().toString(36).slice(2, 9), // Generate temp ID if missing
|
||||
title: event.title,
|
||||
start: new Date(event.startDate.seconds * 1000),
|
||||
end: new Date(event.endDate.seconds * 1000),
|
||||
start: event.allDay
|
||||
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
|
||||
: new Date(event.startDate.seconds * 1000),
|
||||
end: event.allDay
|
||||
? new Date(new Date(event.endDate.seconds * 1000).setHours(0, 0, 0, 0))
|
||||
: new Date(event.endDate.seconds * 1000),
|
||||
hideHours: event.allDay,
|
||||
eventColor,
|
||||
notes: event.notes,
|
||||
|
@ -1,11 +1,34 @@
|
||||
import {useQuery} from "react-query";
|
||||
import { useQuery } from "react-query";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import {useAuthContext} from "@/contexts/AuthContext";
|
||||
import { useAuthContext } from "@/contexts/AuthContext";
|
||||
|
||||
interface FirestoreTimestamp {
|
||||
seconds: number;
|
||||
nanoseconds: number;
|
||||
}
|
||||
|
||||
interface NotificationFirestore {
|
||||
creatorId: string;
|
||||
familyId: string;
|
||||
content: string;
|
||||
eventId: string;
|
||||
timestamp: FirestoreTimestamp;
|
||||
date?: FirestoreTimestamp;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
creatorId: string;
|
||||
familyId: string;
|
||||
content: string;
|
||||
eventId: string;
|
||||
timestamp: Date;
|
||||
date?: Date;
|
||||
}
|
||||
|
||||
export const useGetNotifications = () => {
|
||||
const { user, profileData } = useAuthContext();
|
||||
|
||||
return useQuery({
|
||||
return useQuery<Notification[], Error>({
|
||||
queryKey: ["notifications", user?.uid],
|
||||
queryFn: async () => {
|
||||
const snapshot = await firestore()
|
||||
@ -14,16 +37,14 @@ export const useGetNotifications = () => {
|
||||
.get();
|
||||
|
||||
return snapshot.docs.map((doc) => {
|
||||
const data = doc.data();
|
||||
const data = doc.data() as NotificationFirestore;
|
||||
|
||||
return {...data, timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6)} as {
|
||||
creatorId: string,
|
||||
familyId: string,
|
||||
content: string,
|
||||
eventId: string,
|
||||
timestamp: Date,
|
||||
return {
|
||||
...data,
|
||||
timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6),
|
||||
date: data.date ? new Date(data.date.seconds * 1000 + data.date.nanoseconds / 1e6) : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
@ -48,6 +48,8 @@
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>74</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
@ -155,6 +157,7 @@
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
|
Reference in New Issue
Block a user