Notification changes

This commit is contained in:
Milan Paunovic
2024-11-22 03:25:16 +01:00
parent f74a6390a2
commit 06a3a2dc8f
33 changed files with 1961 additions and 1447 deletions

6
.idea/git_toolbox_blame.xml generated Normal file
View 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>

View 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
View 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>

View File

@ -17,7 +17,10 @@
"bundleIdentifier": "com.cally.app",
"googleServicesFile": "./ios/GoogleService-Info.plist",
"buildNumber": "74",
"usesAppleSignIn": true
"usesAppleSignIn": true,
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"android": {
"adaptiveIcon": {

View File

@ -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",
},
});

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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",
},
});

View File

@ -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() {

View File

@ -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%'}}/>
</>
);

View File

@ -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";

View File

@ -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";

View File

@ -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");

View File

@ -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;

View File

@ -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,
},
});

View File

@ -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;

View File

@ -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 = {

View File

@ -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";

View File

@ -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,
},
});

View File

@ -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;

View File

@ -1,5 +1,4 @@
import React from "react";
import { View } from "react-native";
import { BarChart } from "react-native-gifted-charts";
const FamilyChart = () => {

View File

@ -1,5 +1,4 @@
import React from "react";
import { View } from "react-native";
import { BarChart } from "react-native-gifted-charts";
const UserChart = () => {

View File

@ -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;

View File

@ -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;

View File

@ -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)!;

View File

@ -1,4 +1,3 @@
import { View, Text } from "react-native";
import React, { createContext, useContext, useState } from "react";
export interface IReminder {
id: number;

View File

@ -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);
}
});

View File

@ -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,

View File

@ -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
};
});
}
})
});
};

View File

@ -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>