mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 17:47:08 +00:00
Notification changes
This commit is contained in:
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxBlameSettings">
|
||||||
|
<option name="version" value="2" />
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="t" enabled="true" level="TEXT ATTRIBUTES" enabled_by_default="true" editorAttributes="CONSIDERATION_ATTRIBUTES" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/jsLinters/eslint.xml
generated
Normal file
6
.idea/jsLinters/eslint.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EslintConfiguration">
|
||||||
|
<option name="fix-on-save" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
5
app.json
5
app.json
@ -17,7 +17,10 @@
|
|||||||
"bundleIdentifier": "com.cally.app",
|
"bundleIdentifier": "com.cally.app",
|
||||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||||
"buildNumber": "74",
|
"buildNumber": "74",
|
||||||
"usesAppleSignIn": true
|
"usesAppleSignIn": true,
|
||||||
|
"infoPlist": {
|
||||||
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, {useMemo} from "react";
|
||||||
import { Drawer } from "expo-router/drawer";
|
import {Drawer} from "expo-router/drawer";
|
||||||
import { useSignOut } from "@/hooks/firebase/useSignOut";
|
import {useSignOut} from "@/hooks/firebase/useSignOut";
|
||||||
import { DrawerContentScrollView } from "@react-navigation/drawer";
|
import {DrawerContentScrollView, DrawerNavigationOptions, DrawerNavigationProp} from "@react-navigation/drawer";
|
||||||
import {
|
import {Button, ButtonSize, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
Button,
|
import {ImageBackground, StyleSheet} from "react-native";
|
||||||
ButtonSize,
|
|
||||||
Text,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from "react-native-ui-lib";
|
|
||||||
import { ImageBackground, StyleSheet } from "react-native";
|
|
||||||
import DrawerButton from "@/components/shared/DrawerButton";
|
import DrawerButton from "@/components/shared/DrawerButton";
|
||||||
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
import NavGroceryIcon from "@/assets/svgs/NavGroceryIcon";
|
||||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
@ -17,7 +11,7 @@ import NavBrainDumpIcon from "@/assets/svgs/NavBrainDumpIcon";
|
|||||||
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||||
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
import NavSettingsIcon from "@/assets/svgs/NavSettingsIcon";
|
||||||
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
|
import ViewSwitch from "@/components/pages/(tablet_pages)/ViewSwitch";
|
||||||
import { useSetAtom } from "jotai";
|
import {useSetAtom} from "jotai";
|
||||||
import {
|
import {
|
||||||
isFamilyViewAtom,
|
isFamilyViewAtom,
|
||||||
settingsPageIndex,
|
settingsPageIndex,
|
||||||
@ -26,54 +20,96 @@ import {
|
|||||||
} from "@/components/pages/calendar/atoms";
|
} from "@/components/pages/calendar/atoms";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
import { DeviceType } from "expo-device";
|
import {DeviceType} from "expo-device";
|
||||||
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||||
import DrawerIcon from "@/assets/svgs/DrawerIcon";
|
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() {
|
export default function TabLayout() {
|
||||||
const { mutateAsync: signOut } = useSignOut();
|
const {mutateAsync: signOut} = useSignOut();
|
||||||
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||||
const setPageIndex = useSetAtom(settingsPageIndex);
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
const setUserView = useSetAtom(userSettingsView);
|
const setUserView = useSetAtom(userSettingsView);
|
||||||
const setToDosIndex = useSetAtom(toDosPageIndex);
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
|
|
||||||
return (
|
const screenOptions = useMemo(
|
||||||
<Drawer
|
() =>
|
||||||
initialRouteName={"index"}
|
({
|
||||||
detachInactiveScreens
|
navigation,
|
||||||
screenOptions={({ navigation, route }) => ({
|
route,
|
||||||
|
}: {
|
||||||
|
navigation: DrawerNavigationProp<DrawerParamList>;
|
||||||
|
route: RouteProp<DrawerParamList>;
|
||||||
|
}): DrawerNavigationOptions => ({
|
||||||
headerShown: true,
|
headerShown: true,
|
||||||
headerTitleAlign:
|
headerTitleAlign: Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
||||||
Device.deviceType === DeviceType.TABLET ? "left" : "center",
|
|
||||||
headerTitleStyle: {
|
headerTitleStyle: {
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
fontSize: Device.deviceType === DeviceType.TABLET ? 22 : 17,
|
||||||
},
|
},
|
||||||
headerLeft: (props) => (
|
headerLeft: () => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={navigation.toggleDrawer}
|
onPress={navigation.toggleDrawer}
|
||||||
style={{ marginLeft: 16 }}
|
style={{marginLeft: 16}}
|
||||||
>
|
>
|
||||||
<DrawerIcon />
|
<DrawerIcon/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
headerRight: () => {
|
headerRight: () => {
|
||||||
// Only show ViewSwitch on calendar and todos pages
|
const showViewSwitch = ["calendar", "todos", "index"].includes(route.name);
|
||||||
const showViewSwitch = ["calendar", "todos", "index"].includes(
|
|
||||||
route.name
|
if (Device.deviceType !== DeviceType.TABLET || !showViewSwitch) {
|
||||||
);
|
return null;
|
||||||
return Device.deviceType === DeviceType.TABLET && showViewSwitch ? (
|
}
|
||||||
<View marginR-16>
|
|
||||||
<ViewSwitch navigation={navigation} />
|
return <MemoizedViewSwitch navigation={navigation}/>;
|
||||||
</View>
|
|
||||||
) : null;
|
|
||||||
},
|
},
|
||||||
drawerStyle: {
|
drawerStyle: {
|
||||||
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
|
width: Device.deviceType === DeviceType.TABLET ? "30%" : "90%",
|
||||||
backgroundColor: "#f9f8f7",
|
backgroundColor: "#f9f8f7",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
})}
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
initialRouteName={"index"}
|
||||||
|
detachInactiveScreens
|
||||||
|
screenOptions={screenOptions}
|
||||||
drawerContent={(props) => {
|
drawerContent={(props) => {
|
||||||
return (
|
return (
|
||||||
<DrawerContentScrollView {...props} style={{}}>
|
<DrawerContentScrollView {...props} style={{}}>
|
||||||
@ -95,7 +131,7 @@ export default function TabLayout() {
|
|||||||
paddingHorizontal: 30,
|
paddingHorizontal: 30,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, paddingRight: 5 }}>
|
<View style={{flex: 1, paddingRight: 5}}>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
title={"Calendar"}
|
title={"Calendar"}
|
||||||
color="rgb(7, 184, 199)"
|
color="rgb(7, 184, 199)"
|
||||||
@ -107,7 +143,7 @@ export default function TabLayout() {
|
|||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavCalendarIcon />}
|
icon={<NavCalendarIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#50be0c"
|
color="#50be0c"
|
||||||
@ -120,7 +156,7 @@ export default function TabLayout() {
|
|||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavGroceryIcon />}
|
icon={<NavGroceryIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#ea156d"
|
color="#ea156d"
|
||||||
@ -133,10 +169,10 @@ export default function TabLayout() {
|
|||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<FeedbackNavIcon />}
|
icon={<FeedbackNavIcon/>}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1, paddingRight: 0 }}>
|
<View style={{flex: 1, paddingRight: 0}}>
|
||||||
{/*<DrawerButton
|
{/*<DrawerButton
|
||||||
color="#fd1775"
|
color="#fd1775"
|
||||||
title={"My Reminders"}
|
title={"My Reminders"}
|
||||||
@ -161,7 +197,7 @@ export default function TabLayout() {
|
|||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavToDosIcon />}
|
icon={<NavToDosIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#e0ca03"
|
color="#e0ca03"
|
||||||
@ -174,7 +210,7 @@ export default function TabLayout() {
|
|||||||
setUserView(true);
|
setUserView(true);
|
||||||
setIsFamilyView(false);
|
setIsFamilyView(false);
|
||||||
}}
|
}}
|
||||||
icon={<NavBrainDumpIcon />}
|
icon={<NavBrainDumpIcon/>}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#e0ca03"
|
color="#e0ca03"
|
||||||
@ -212,12 +248,12 @@ export default function TabLayout() {
|
|||||||
backgroundColor="#ededed"
|
backgroundColor="#ededed"
|
||||||
width={60}
|
width={60}
|
||||||
height={60}
|
height={60}
|
||||||
style={{ borderRadius: 50 }}
|
style={{borderRadius: 50}}
|
||||||
marginR-10
|
marginR-10
|
||||||
centerV
|
centerV
|
||||||
centerH
|
centerH
|
||||||
>
|
>
|
||||||
<NavSettingsIcon />
|
<NavSettingsIcon/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
@ -225,7 +261,7 @@ export default function TabLayout() {
|
|||||||
paddingV-30
|
paddingV-30
|
||||||
marginH-30
|
marginH-30
|
||||||
borderRadius={18.55}
|
borderRadius={18.55}
|
||||||
style={{ elevation: 0 }}
|
style={{elevation: 0}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -266,7 +302,7 @@ export default function TabLayout() {
|
|||||||
Device.deviceType === DeviceType.TABLET
|
Device.deviceType === DeviceType.TABLET
|
||||||
? "Family Calendar"
|
? "Family Calendar"
|
||||||
: "Calendar",
|
: "Calendar",
|
||||||
drawerItemStyle: { display: "none" },
|
drawerItemStyle: {display: "none"},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
@ -316,15 +352,15 @@ export default function TabLayout() {
|
|||||||
/>
|
/>
|
||||||
<Drawer.Screen
|
<Drawer.Screen
|
||||||
name="feedback"
|
name="feedback"
|
||||||
options={{ drawerLabel: "Feedback", title: "Feedback" }}
|
options={{drawerLabel: "Feedback", title: "Feedback"}}
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
signOut: { fontFamily: "Poppins_500Medium", fontSize: 15 },
|
signOut: {fontFamily: "Poppins_500Medium", fontSize: 15},
|
||||||
label: { fontFamily: "Poppins_400Medium", fontSize: 15 },
|
label: {fontFamily: "Poppins_400Medium", fontSize: 15},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 26.13,
|
fontSize: 26.13,
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
@ -1,79 +1,28 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { ScrollView, RefreshControl, View } from "react-native";
|
import {RefreshControl, ScrollView, View} from "react-native";
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import CalendarPage from "@/components/pages/calendar/CalendarPage";
|
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 TabletCalendarPage from "@/components/pages/(tablet_pages)/calendar/TabletCalendarPage";
|
||||||
import { DeviceType } from "expo-device";
|
|
||||||
import * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
import { useCalSync } from "@/hooks/useCalSync";
|
import {DeviceType} from "expo-device";
|
||||||
import Toast from "react-native-toast-message";
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const isTablet = Device.deviceType === DeviceType.TABLET;
|
||||||
const [shouldRefresh, setShouldRefresh] = useAtom(refreshTriggerAtom);
|
|
||||||
|
|
||||||
const isTablet: boolean = Device.deviceType === DeviceType.TABLET;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnectedToGoogle,
|
|
||||||
isConnectedToMicrosoft,
|
|
||||||
isConnectedToApple,
|
|
||||||
resyncAllCalendars,
|
resyncAllCalendars,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
} = useCalSync();
|
} = useCalSync();
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = React.useCallback(async () => {
|
||||||
setRefreshing(true);
|
|
||||||
|
|
||||||
const minimumDelay = new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isConnectedToGoogle || isConnectedToMicrosoft || isConnectedToApple) {
|
await resyncAllCalendars();
|
||||||
await Promise.all([resyncAllCalendars(), minimumDelay]);
|
|
||||||
} else {
|
|
||||||
await minimumDelay;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Refresh failed:", error);
|
console.error("Refresh failed:", error);
|
||||||
} finally {
|
|
||||||
setRefreshing(false);
|
|
||||||
setShouldRefresh((prev) => !prev);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [resyncAllCalendars]);
|
||||||
resyncAllCalendars,
|
|
||||||
isConnectedToGoogle,
|
|
||||||
isConnectedToMicrosoft,
|
|
||||||
isConnectedToApple,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
const refreshControl = (
|
||||||
<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
|
<RefreshControl
|
||||||
colors={[
|
colors={[
|
||||||
colorMap.pink,
|
colorMap.pink,
|
||||||
@ -83,23 +32,23 @@ export default function Screen() {
|
|||||||
colorMap.teal,
|
colorMap.teal,
|
||||||
]}
|
]}
|
||||||
tintColor={colorMap.pink}
|
tintColor={colorMap.pink}
|
||||||
progressBackgroundColor={"white"}
|
progressBackgroundColor="white"
|
||||||
refreshing={refreshing || isSyncing}
|
refreshing={isSyncing}
|
||||||
onRefresh={onRefresh}
|
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
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView
|
||||||
|
style={{flex: 1, height: "100%",}}
|
||||||
|
contentContainerStyle={{flex: 1, height: "100%"}}
|
||||||
|
refreshControl={refreshControl}
|
||||||
bounces={true}
|
bounces={true}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
pointerEvents={refreshing || isSyncing ? "auto" : "none"}
|
>
|
||||||
/>
|
<View style={{flex: 1}}>
|
||||||
|
{isTablet ? <TabletCalendarPage/> : <CalendarPage/>}
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,24 +1,15 @@
|
|||||||
import TabletChoresPage from "@/components/pages/(tablet_pages)/chores/TabletChoresPage";
|
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 ToDosPage from "@/components/pages/todos/ToDosPage";
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
import {ToDosContextProvider} from "@/contexts/ToDosContext";
|
||||||
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 * as Device from "expo-device";
|
import * as Device from "expo-device";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
return (
|
return (
|
||||||
<ToDosContextProvider>
|
<ToDosContextProvider>
|
||||||
{Device.deviceType === Device.DeviceType.TABLET ? (
|
{Device.deviceType === Device.DeviceType.TABLET ? (
|
||||||
<TabletChoresPage />
|
<TabletChoresPage/>
|
||||||
) : (
|
) : (
|
||||||
<ToDosPage />
|
<ToDosPage/>
|
||||||
)}
|
)}
|
||||||
</ToDosContextProvider>
|
</ToDosContextProvider>
|
||||||
);
|
);
|
||||||
|
@ -1,43 +1,21 @@
|
|||||||
import { SegmentedControl, View } from "react-native-ui-lib";
|
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 { StyleSheet } from "react-native";
|
||||||
import { NavigationProp, useNavigationState } from "@react-navigation/native";
|
import { NavigationProp, useNavigationState } from "@react-navigation/native";
|
||||||
|
|
||||||
const ViewSwitch = memo(function ViewSwitch({
|
interface ViewSwitchProps {
|
||||||
navigation,
|
navigation: NavigationProp<any>;
|
||||||
}: {
|
}
|
||||||
navigation: any;
|
|
||||||
}) {
|
|
||||||
const isNavigating = useRef(false);
|
|
||||||
const navigationState = useNavigationState((state) => state);
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(
|
|
||||||
navigationState.index === 6 ? 1 : 0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sync the selected index with navigation state
|
const ViewSwitch = memo(function ViewSwitch({ navigation }: ViewSwitchProps) {
|
||||||
useEffect(() => {
|
const currentIndex = useNavigationState((state) => state.index === 6 ? 1 : 0);
|
||||||
const newIndex = navigationState.index === 6 ? 1 : 0;
|
|
||||||
if (selectedIndex !== newIndex) {
|
|
||||||
setSelectedIndex(newIndex);
|
|
||||||
}
|
|
||||||
isNavigating.current = false;
|
|
||||||
}, [navigationState.index]);
|
|
||||||
|
|
||||||
const handleSegmentChange = useCallback(
|
const handleSegmentChange = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
if (isNavigating.current) return;
|
if (index === currentIndex) return;
|
||||||
if (index === selectedIndex) return;
|
|
||||||
|
|
||||||
isNavigating.current = true;
|
|
||||||
setSelectedIndex(index);
|
|
||||||
|
|
||||||
// Delay navigation slightly to allow the segment control to update
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
navigation.navigate(index === 0 ? "calendar" : "todos");
|
navigation.navigate(index === 0 ? "calendar" : "todos");
|
||||||
});
|
|
||||||
console.log(selectedIndex)
|
|
||||||
},
|
},
|
||||||
[navigation, selectedIndex]
|
[navigation, currentIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,12 +34,11 @@ const ViewSwitch = memo(function ViewSwitch({
|
|||||||
outlineColor="white"
|
outlineColor="white"
|
||||||
outlineWidth={3}
|
outlineWidth={3}
|
||||||
onChangeIndex={handleSegmentChange}
|
onChangeIndex={handleSegmentChange}
|
||||||
initialIndex={selectedIndex}
|
initialIndex={currentIndex}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export default ViewSwitch;
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@ -87,3 +64,5 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default ViewSwitch;
|
@ -1,17 +1,14 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, {useEffect} from "react";
|
||||||
import { View, Text } from "react-native-ui-lib";
|
import {Text, View} from "react-native-ui-lib";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import TabletContainer from "../tablet_components/TabletContainer";
|
import TabletContainer from "../tablet_components/TabletContainer";
|
||||||
import ToDosPage from "../../todos/ToDosPage";
|
|
||||||
import ToDosList from "../../todos/ToDosList";
|
|
||||||
import SingleUserChoreList from "./SingleUserChoreList";
|
import SingleUserChoreList from "./SingleUserChoreList";
|
||||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
import { ImageBackground, StyleSheet } from "react-native";
|
import {ImageBackground, StyleSheet} from "react-native";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import {ScrollView} from "react-native-gesture-handler";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
|
||||||
|
|
||||||
const TabletChoresPage = () => {
|
const TabletChoresPage = () => {
|
||||||
const { data: users } = useGetFamilyMembers();
|
const {data: users} = useGetFamilyMembers();
|
||||||
// Function to lock the screen orientation to landscape
|
// Function to lock the screen orientation to landscape
|
||||||
const lockScreenOrientation = async () => {
|
const lockScreenOrientation = async () => {
|
||||||
await ScreenOrientation.lockAsync(
|
await ScreenOrientation.lockAsync(
|
||||||
@ -37,7 +34,7 @@ const TabletChoresPage = () => {
|
|||||||
<View row centerV>
|
<View row centerV>
|
||||||
{user.pfp ? (
|
{user.pfp ? (
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
source={{ uri: user.pfp }}
|
source={{uri: user.pfp}}
|
||||||
style={[
|
style={[
|
||||||
styles.pfp,
|
styles.pfp,
|
||||||
(user.eventColor && {
|
(user.eventColor && {
|
||||||
@ -63,11 +60,11 @@ const TabletChoresPage = () => {
|
|||||||
<Text style={styles.name} marginL-15>
|
<Text style={styles.name} marginL-15>
|
||||||
{user.firstName}
|
{user.firstName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.name, { color: "#9b9b9b" }]} marginL-5>
|
<Text style={[styles.name, {color: "#9b9b9b"}]} marginL-5>
|
||||||
({user.userType})
|
({user.userType})
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<SingleUserChoreList user={user} />
|
<SingleUserChoreList user={user}/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
@ -3,7 +3,6 @@ import React from "react";
|
|||||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
import BrainDumpItem from "./DumpItem";
|
import BrainDumpItem from "./DumpItem";
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
|
||||||
|
|
||||||
const DumpList = (props: { searchText: string }) => {
|
const DumpList = (props: { searchText: string }) => {
|
||||||
const { brainDumps } = useBrainDumpContext();
|
const { brainDumps } = useBrainDumpContext();
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
import React, { memo } from "react";
|
import React, {memo} from "react";
|
||||||
import {
|
import {Button, Picker, PickerModes, SegmentedControl, Text, View,} from "react-native-ui-lib";
|
||||||
Button,
|
import {MaterialIcons} from "@expo/vector-icons";
|
||||||
Picker,
|
import {modeMap, months} from "./constants";
|
||||||
PickerModes,
|
import {StyleSheet} from "react-native";
|
||||||
SegmentedControl,
|
import {useAtom} from "jotai";
|
||||||
Text,
|
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||||
View,
|
import {format, isSameDay} from "date-fns";
|
||||||
} 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";
|
|
||||||
|
|
||||||
export const CalendarHeader = memo(() => {
|
export const CalendarHeader = memo(() => {
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
const [mode, setMode] = useAtom(modeAtom);
|
const [mode, setMode] = useAtom(modeAtom);
|
||||||
const { profileData } = useAuthContext();
|
|
||||||
|
|
||||||
const handleSegmentChange = (index: number) => {
|
const handleSegmentChange = (index: number) => {
|
||||||
const selectedMode = modeMap.get(index);
|
const selectedMode = modeMap.get(index);
|
||||||
@ -57,23 +47,23 @@ export const CalendarHeader = memo(() => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View row centerV gap-3>
|
<View row centerV gap-3>
|
||||||
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 17 }}>
|
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 17}}>
|
||||||
{selectedDate.getFullYear()}
|
{selectedDate.getFullYear()}
|
||||||
</Text>
|
</Text>
|
||||||
<Picker
|
<Picker
|
||||||
value={months[selectedDate.getMonth()]}
|
value={months[selectedDate.getMonth()]}
|
||||||
placeholder={"Select Month"}
|
placeholder={"Select Month"}
|
||||||
style={{ fontFamily: "Manrope_500Medium", fontSize: 17, width: 85 }}
|
style={{fontFamily: "Manrope_500Medium", fontSize: 17, width: 85}}
|
||||||
mode={PickerModes.SINGLE}
|
mode={PickerModes.SINGLE}
|
||||||
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
onChange={(itemValue) => handleMonthChange(itemValue as string)}
|
||||||
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"} />}
|
trailingAccessory={<MaterialIcons name={"keyboard-arrow-down"}/>}
|
||||||
topBarProps={{
|
topBarProps={{
|
||||||
title: selectedDate.getFullYear().toString(),
|
title: selectedDate.getFullYear().toString(),
|
||||||
titleStyle: { fontFamily: "Manrope_500Medium", fontSize: 17 },
|
titleStyle: {fontFamily: "Manrope_500Medium", fontSize: 17},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{months.map((month) => (
|
{months.map((month) => (
|
||||||
<Picker.Item key={month} label={month} value={month} />
|
<Picker.Item key={month} label={month} value={month}/>
|
||||||
))}
|
))}
|
||||||
</Picker>
|
</Picker>
|
||||||
</View>
|
</View>
|
||||||
@ -106,7 +96,7 @@ export const CalendarHeader = memo(() => {
|
|||||||
|
|
||||||
<View>
|
<View>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
segments={[{ label: "D" }, { label: "W" }, { label: "M" }]}
|
segments={[{label: "D"}, {label: "W"}, {label: "M"}]}
|
||||||
backgroundColor="#ececec"
|
backgroundColor="#ececec"
|
||||||
inactiveColor="#919191"
|
inactiveColor="#919191"
|
||||||
activeBackgroundColor="#ea156c"
|
activeBackgroundColor="#ea156c"
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native-ui-lib";
|
import { View } from "react-native-ui-lib";
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
|
||||||
import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
|
import { InnerCalendar } from "@/components/pages/calendar/InnerCalendar";
|
||||||
|
|
||||||
export default function CalendarPage() {
|
export default function CalendarPage() {
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||||
import { Calendar } from "react-native-big-calendar";
|
import {Calendar} from "react-native-big-calendar";
|
||||||
import { ActivityIndicator, ScrollView, StyleSheet, View, ViewStyle } from "react-native";
|
import {ActivityIndicator, StyleSheet, View, ViewStyle} from "react-native";
|
||||||
import { useGetEvents } from "@/hooks/firebase/useGetEvents";
|
import {useGetEvents} from "@/hooks/firebase/useGetEvents";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
import {
|
import {
|
||||||
editVisibleAtom,
|
editVisibleAtom,
|
||||||
eventForEditAtom,
|
eventForEditAtom,
|
||||||
isAllDayAtom, isFamilyViewAtom,
|
isAllDayAtom,
|
||||||
|
isFamilyViewAtom,
|
||||||
modeAtom,
|
modeAtom,
|
||||||
refreshTriggerAtom,
|
|
||||||
selectedDateAtom,
|
selectedDateAtom,
|
||||||
selectedNewEventDateAtom,
|
selectedNewEventDateAtom,
|
||||||
} from "@/components/pages/calendar/atoms";
|
} from "@/components/pages/calendar/atoms";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
||||||
import { Text } from "react-native-ui-lib";
|
import {Text} from "react-native-ui-lib";
|
||||||
import { addDays, compareAsc, isWithinInterval, subDays } from "date-fns";
|
import {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
|
||||||
import {useCalSync} from "@/hooks/useCalSync";
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
import {useSyncEvents} from "@/hooks/useSyncOnScroll";
|
||||||
import {colorMap} from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
@ -32,9 +32,9 @@ const getTotalMinutes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||||
({ calendarHeight }) => {
|
({calendarHeight}) => {
|
||||||
const { data: events, isLoading, refetch } = useGetEvents();
|
const {data: events, isLoading, refetch} = useGetEvents();
|
||||||
const { profileData, user } = useAuthContext();
|
const {profileData, user} = useAuthContext();
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
const [mode, setMode] = useAtom(modeAtom);
|
const [mode, setMode] = useAtom(modeAtom);
|
||||||
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
@ -42,7 +42,6 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const setEditVisible = useSetAtom(editVisibleAtom);
|
const setEditVisible = useSetAtom(editVisibleAtom);
|
||||||
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
const [isAllDay, setIsAllDay] = useAtom(isAllDayAtom);
|
||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
const shouldRefresh = useAtomValue(refreshTriggerAtom);
|
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
const {isSyncing} = useSyncEvents()
|
const {isSyncing} = useSyncEvents()
|
||||||
@ -84,8 +83,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
setSelectedNewEndDate(date);
|
setSelectedNewEndDate(date);
|
||||||
setEditVisible(true);
|
setEditVisible(true);
|
||||||
}
|
}
|
||||||
if (mode === 'week')
|
if (mode === 'week') {
|
||||||
{
|
|
||||||
setSelectedDate(date)
|
setSelectedDate(date)
|
||||||
|
|
||||||
setMode("day")
|
setMode("day")
|
||||||
@ -108,7 +106,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
eventColor = profileData?.eventColor ?? colorMap.teal;
|
eventColor = profileData?.eventColor ?? colorMap.teal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { backgroundColor: eventColor , fontSize: 14}
|
return {backgroundColor: eventColor, fontSize: 14}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@ -118,8 +116,6 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
[profileData]
|
[profileData]
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek,
|
|
||||||
|
|
||||||
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||||
return (
|
return (
|
||||||
date1.getDate() === date2.getDate() &&
|
date1.getDate() === date2.getDate() &&
|
||||||
@ -151,7 +147,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
}
|
}
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
const { enrichedEvents, filteredEvents } = useMemo(() => {
|
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||||
const startTime = Date.now(); // Start timer
|
const startTime = Date.now(); // Start timer
|
||||||
|
|
||||||
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||||
@ -189,7 +185,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
// console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
|
|
||||||
return { enrichedEvents, filteredEvents };
|
return {enrichedEvents, filteredEvents};
|
||||||
}, [events, selectedDate, mode]);
|
}, [events, selectedDate, mode]);
|
||||||
|
|
||||||
const renderCustomDateForMonth = (date: Date) => {
|
const renderCustomDateForMonth = (date: Date) => {
|
||||||
@ -225,9 +221,9 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ alignItems: "center" }}>
|
<View style={{alignItems: "center"}}>
|
||||||
<View style={appliedStyle}>
|
<View style={appliedStyle}>
|
||||||
<Text style={{ color: isCurrentDate ? "white" : "black" }}>
|
<Text style={{color: isCurrentDate ? "white" : "black"}}>
|
||||||
{date.getDate()}
|
{date.getDate()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -244,17 +240,6 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
setOffsetMinutes(getTotalMinutes());
|
setOffsetMinutes(getTotalMinutes());
|
||||||
}, [events, mode]);
|
}, [events, mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refetch()
|
|
||||||
.then(() => {
|
|
||||||
console.log('✅ Events refreshed successfully');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('❌ Events refresh failed:', error);
|
|
||||||
});
|
|
||||||
}, [shouldRefresh, refetch])
|
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
@ -317,14 +302,14 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
|||||||
dayHeaderHighlightColor={"white"}
|
dayHeaderHighlightColor={"white"}
|
||||||
showAdjacentMonths
|
showAdjacentMonths
|
||||||
headerContainerStyle={mode !== "month" ? {
|
headerContainerStyle={mode !== "month" ? {
|
||||||
overflow:"hidden",
|
overflow: "hidden",
|
||||||
} : {}}
|
} : {}}
|
||||||
hourStyle={styles.hourStyle}
|
hourStyle={styles.hourStyle}
|
||||||
onPressDateHeader={handlePressDayHeader}
|
onPressDateHeader={handlePressDayHeader}
|
||||||
ampm
|
ampm
|
||||||
// renderCustomDateForMonth={renderCustomDateForMonth}
|
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||||
/>
|
/>
|
||||||
<View style={{backgroundColor: 'white', height: 50, width: '100%'}} />
|
<View style={{backgroundColor: 'white', height: 50, width: '100%'}}/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
@ -11,7 +11,6 @@ import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView
|
|||||||
import { Dimensions, Platform, StyleSheet } from "react-native";
|
import { Dimensions, Platform, StyleSheet } from "react-native";
|
||||||
|
|
||||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
|
||||||
import KeyboardManager from "react-native-keyboard-manager";
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
import { useFeedbackContext } from "@/contexts/FeedbackContext";
|
import { useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
@ -14,9 +14,6 @@ import PenIcon from "@/assets/svgs/PenIcon";
|
|||||||
import BinIcon from "@/assets/svgs/BinIcon";
|
import BinIcon from "@/assets/svgs/BinIcon";
|
||||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
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 MenuIcon from "@/assets/svgs/MenuIcon";
|
||||||
import { IFeedback, useFeedbackContext } from "@/contexts/FeedbackContext";
|
import { IFeedback, useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||||
import FeedbackDialog from "./FeedbackDialog";
|
import FeedbackDialog from "./FeedbackDialog";
|
||||||
|
@ -2,7 +2,6 @@ import {Dimensions, StyleSheet} from "react-native";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, View,} from "react-native-ui-lib";
|
import {Button, View,} from "react-native-ui-lib";
|
||||||
import {useGroceryContext} from "@/contexts/GroceryContext";
|
import {useGroceryContext} from "@/contexts/GroceryContext";
|
||||||
import {FontAwesome6} from "@expo/vector-icons";
|
|
||||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
const { width } = Dimensions.get("screen");
|
const { width } = Dimensions.get("screen");
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
import {FlatList, StyleSheet} from "react-native";
|
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 {Card, Text, View} from "react-native-ui-lib";
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
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 {formatDistanceToNow} from "date-fns";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
import {useSetAtom} from "jotai";
|
||||||
|
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
||||||
|
|
||||||
const NotificationsPage = () => {
|
const NotificationsPage = () => {
|
||||||
const {data: notifications} = useGetNotifications()
|
const setSelectedDate = useSetAtom(selectedDateAtom);
|
||||||
|
const setMode = useSetAtom(modeAtom);
|
||||||
console.log(notifications?.[0])
|
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 (
|
return (
|
||||||
@ -18,32 +29,56 @@ const NotificationsPage = () => {
|
|||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Welcome to your notifications!"}
|
message={"Welcome to your notifications!"}
|
||||||
isWelcome={false}
|
isWelcome={false}
|
||||||
children={
|
|
||||||
<Text
|
|
||||||
style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
|
|
||||||
>
|
>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
See your notifications here.
|
See your notifications here.
|
||||||
</Text>
|
</Text>
|
||||||
}
|
</HeaderTemplate>
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FlatList contentContainerStyle={{paddingBottom: 10, paddingHorizontal: 25}}
|
<FlatList
|
||||||
|
contentContainerStyle={styles.listContainer}
|
||||||
data={notifications ?? []}
|
data={notifications ?? []}
|
||||||
renderItem={({item}) => <Card padding-20 gap-10 marginB-10>
|
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>
|
<Text text70>{item.content}</Text>
|
||||||
<View row spread>
|
<View row spread marginT-10>
|
||||||
<Text
|
<Text text90>
|
||||||
text90>{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}</Text>
|
{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}
|
||||||
<Text text90>{item.timestamp.toLocaleDateString()}</Text>
|
</Text>
|
||||||
|
<Text text90>
|
||||||
|
{item.timestamp.toLocaleDateString()}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Card>}/>
|
</Card>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
listContainer: {
|
||||||
|
paddingBottom: 10,
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontFamily: "Manrope_400Regular",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
searchField: {
|
searchField: {
|
||||||
borderWidth: 0.7,
|
borderWidth: 0.7,
|
||||||
borderColor: "#9b9b9b",
|
borderColor: "#9b9b9b",
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
import { Image } from "react-native";
|
import {Image, StyleSheet} from "react-native";
|
||||||
import React, { useRef } from "react";
|
import React, {useRef} from "react";
|
||||||
import { View, Text, Button, TextField } from "react-native-ui-lib";
|
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||||
import Onboarding from "react-native-onboarding-swiper";
|
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 OnboardingFlow = () => {
|
||||||
const onboardingRef = useRef(null);
|
const onboardingRef = useRef(null);
|
||||||
const { mutateAsync: signUp } = useSignUp();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Onboarding
|
<Onboarding
|
||||||
showPagination={false}
|
showPagination={false}
|
||||||
ref={onboardingRef}
|
ref={onboardingRef}
|
||||||
containerStyles={{ backgroundColor: "#f9f8f7" }}
|
containerStyles={{backgroundColor: "#f9f8f7"}}
|
||||||
imageContainerStyles={{
|
imageContainerStyles={{
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
@ -34,7 +31,7 @@ const OnboardingFlow = () => {
|
|||||||
<Text text50R>Lightening Mental Loads, One Family at a Time</Text>
|
<Text text50R>Lightening Mental Loads, One Family at a Time</Text>
|
||||||
<Button
|
<Button
|
||||||
label="Continue"
|
label="Continue"
|
||||||
style={{ backgroundColor: "#fd1775" }}
|
style={{backgroundColor: "#fd1775"}}
|
||||||
onPress={() => onboardingRef?.current?.goToPage(1, true)}
|
onPress={() => onboardingRef?.current?.goToPage(1, true)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -60,8 +57,8 @@ const OnboardingFlow = () => {
|
|||||||
<View marginH-30>
|
<View marginH-30>
|
||||||
{/*<TextField style={styles.textfield} placeholder="First name" />*/}
|
{/*<TextField style={styles.textfield} placeholder="First name" />*/}
|
||||||
{/*<TextField style={styles.textfield} placeholder="Last name" />*/}
|
{/*<TextField style={styles.textfield} placeholder="Last name" />*/}
|
||||||
<TextField style={styles.textfield} placeholder="Email" />
|
<TextField style={styles.textfield} placeholder="Email"/>
|
||||||
<TextField style={styles.textfield} placeholder="Password" />
|
<TextField style={styles.textfield} placeholder="Password"/>
|
||||||
<Button
|
<Button
|
||||||
label="Login"
|
label="Login"
|
||||||
backgroundColor="#ea156c"
|
backgroundColor="#ea156c"
|
||||||
|
@ -2,7 +2,6 @@ import React, { useState } from "react";
|
|||||||
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
import {useDeleteUser} from "@/hooks/firebase/useDeleteUser";
|
|
||||||
|
|
||||||
interface ConfirmationDialogProps {
|
interface ConfirmationDialogProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -33,8 +33,6 @@ import KeyboardManager, {
|
|||||||
} from "react-native-keyboard-manager";
|
} from "react-native-keyboard-manager";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import { useUploadProfilePicture } from "@/hooks/useUploadProfilePicture";
|
import { useUploadProfilePicture } from "@/hooks/useUploadProfilePicture";
|
||||||
import { ImagePickerAsset } from "expo-image-picker";
|
|
||||||
import MenuDotsIcon from "@/assets/svgs/MenuDotsIcon";
|
|
||||||
import UserOptions from "./UserOptions";
|
import UserOptions from "./UserOptions";
|
||||||
|
|
||||||
type MyGroupProps = {
|
type MyGroupProps = {
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
import QRCode from "react-native-qrcode-svg";
|
import QRCode from "react-native-qrcode-svg";
|
||||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/components/panningViews/panningProvider";
|
import { PanningDirectionsEnum } from "react-native-ui-lib/src/components/panningViews/panningProvider";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
|
||||||
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import { ProfileType } from "@/contexts/AuthContext";
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Dimensions, StyleSheet } from "react-native";
|
import {StyleSheet} from "react-native";
|
||||||
import React, { useState } from "react";
|
import React, {useState} from "react";
|
||||||
import { Button, ButtonSize, Text, View } from "react-native-ui-lib";
|
import {Button, ButtonSize, Text, View} from "react-native-ui-lib";
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
|
||||||
import AddChoreDialog from "./AddChoreDialog";
|
import AddChoreDialog from "./AddChoreDialog";
|
||||||
import PlusIcon from "@/assets/svgs/PlusIcon";
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
@ -27,10 +25,10 @@ const AddChore = () => {
|
|||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={() => setIsVisible(!isVisible)}
|
onPress={() => setIsVisible(!isVisible)}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon/>
|
||||||
<Text
|
<Text
|
||||||
white
|
white
|
||||||
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 15 }}
|
style={{fontFamily: "Manrope_600SemiBold", fontSize: 15}}
|
||||||
marginL-5
|
marginL-5
|
||||||
>
|
>
|
||||||
Create new to do
|
Create new to do
|
||||||
@ -38,7 +36,7 @@ const AddChore = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
|
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible}/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
import { View, Text, Button } from "react-native-ui-lib";
|
import {Text, View} from "react-native-ui-lib";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Fontisto } from "@expo/vector-icons";
|
import {ProgressBar} from "react-native-ui-lib/src/components/progressBar";
|
||||||
import { ProgressBar } from "react-native-ui-lib/src/components/progressBar";
|
import {useToDosContext} from "@/contexts/ToDosContext";
|
||||||
import { useToDosContext } from "@/contexts/ToDosContext";
|
|
||||||
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
||||||
|
|
||||||
const ProgressCard = ({children}: {children?: React.ReactNode}) => {
|
const ProgressCard = ({children}: { children?: React.ReactNode }) => {
|
||||||
const { maxPoints } = useToDosContext();
|
const {maxPoints} = useToDosContext();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
marginB-5
|
marginB-5
|
||||||
padding-15
|
padding-15
|
||||||
style={{ borderRadius: 22 }}
|
style={{borderRadius: 22}}
|
||||||
>
|
>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<FireworksOrangeIcon />
|
<FireworksOrangeIcon/>
|
||||||
<Text marginL-15 text70 style={{fontFamily: 'Manrope_600SemiBold', fontSize: 14}}>
|
<Text marginL-15 text70 style={{fontFamily: 'Manrope_600SemiBold', fontSize: 14}}>
|
||||||
You have earned XX points this week!{" "}
|
You have earned XX points this week!{" "}
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
|
||||||
import { BarChart } from "react-native-gifted-charts";
|
import { BarChart } from "react-native-gifted-charts";
|
||||||
|
|
||||||
const FamilyChart = () => {
|
const FamilyChart = () => {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { View } from "react-native";
|
|
||||||
import { BarChart } from "react-native-gifted-charts";
|
import { BarChart } from "react-native-gifted-charts";
|
||||||
|
|
||||||
const UserChart = () => {
|
const UserChart = () => {
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
import {
|
import {Button, ButtonSize, Dialog, ProgressBar, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
View,
|
import React, {useState} from "react";
|
||||||
Text,
|
import {StyleSheet} from "react-native";
|
||||||
ProgressBar,
|
|
||||||
Button,
|
|
||||||
ButtonSize,
|
|
||||||
Modal,
|
|
||||||
Dialog,
|
|
||||||
TouchableOpacity,
|
|
||||||
} from "react-native-ui-lib";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { StyleSheet } from "react-native";
|
|
||||||
import UserChart from "./UserChart";
|
import UserChart from "./UserChart";
|
||||||
import ProgressCard from "../ProgressCard";
|
import ProgressCard from "../ProgressCard";
|
||||||
import { AntDesign, Feather, Ionicons } from "@expo/vector-icons";
|
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import {ScrollView} from "react-native-gesture-handler";
|
||||||
import { PanViewDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
|
||||||
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
import FireworksOrangeIcon from "@/assets/svgs/FireworksOrangeIcon";
|
||||||
|
|
||||||
const UserChoresProgress = ({
|
const UserChoresProgress = ({
|
||||||
setPageIndex,
|
setPageIndex,
|
||||||
}: {
|
}: {
|
||||||
setPageIndex: (value: number) => void;
|
setPageIndex: (value: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
@ -35,10 +25,10 @@ const UserChoresProgress = ({
|
|||||||
name="chevron-back"
|
name="chevron-back"
|
||||||
size={14}
|
size={14}
|
||||||
color="#979797"
|
color="#979797"
|
||||||
style={{ paddingBottom: 3 }}
|
style={{paddingBottom: 3}}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
|
||||||
color="#979797"
|
color="#979797"
|
||||||
>
|
>
|
||||||
Return to To Do's
|
Return to To Do's
|
||||||
@ -46,26 +36,26 @@ const UserChoresProgress = ({
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
<Text style={{fontFamily: "Manrope_700Bold", fontSize: 20}}>
|
||||||
Your To Do's Progress Report
|
Your To Do's Progress Report
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View row spread marginT-25 marginB-5>
|
<View row spread marginT-25 marginB-5>
|
||||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||||
Daily Goal
|
Daily Goal
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<ProgressCard />
|
<ProgressCard/>
|
||||||
<View row spread marginT-15 marginB-8>
|
<View row spread marginT-15 marginB-8>
|
||||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||||
Points Earned This Week
|
Points Earned This Week
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.card} paddingL-10>
|
<View style={styles.card} paddingL-10>
|
||||||
<UserChart />
|
<UserChart/>
|
||||||
</View>
|
</View>
|
||||||
<View row spread marginT-20 marginB-8 centerV>
|
<View row spread marginT-20 marginB-8 centerV>
|
||||||
<Text text70 style={{ fontFamily: "Manrope_600SemiBold" }}>
|
<Text text70 style={{fontFamily: "Manrope_600SemiBold"}}>
|
||||||
Total Reward Points
|
Total Reward Points
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
@ -82,7 +72,7 @@ const UserChoresProgress = ({
|
|||||||
<AntDesign
|
<AntDesign
|
||||||
name="gift"
|
name="gift"
|
||||||
size={20}
|
size={20}
|
||||||
style={{ marginRight: 5 }}
|
style={{marginRight: 5}}
|
||||||
color="#50be0c"
|
color="#50be0c"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -90,11 +80,11 @@ const UserChoresProgress = ({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<FireworksOrangeIcon color="#8005eb" />
|
<FireworksOrangeIcon color="#8005eb"/>
|
||||||
<Text
|
<Text
|
||||||
marginL-8
|
marginL-8
|
||||||
text70
|
text70
|
||||||
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
|
style={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
|
||||||
>
|
>
|
||||||
You have 1200 points saved!
|
You have 1200 points saved!
|
||||||
</Text>
|
</Text>
|
||||||
@ -110,8 +100,8 @@ const UserChoresProgress = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View row spread>
|
<View row spread>
|
||||||
<Text style={{ fontSize: 13, color: "#858585" }}>0</Text>
|
<Text style={{fontSize: 13, color: "#858585"}}>0</Text>
|
||||||
<Text style={{ fontSize: 13, color: "#858585" }}>5000</Text>
|
<Text style={{fontSize: 13, color: "#858585"}}>5000</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -156,7 +146,7 @@ const UserChoresProgress = ({
|
|||||||
labelStyle={styles.bigButtonText}
|
labelStyle={styles.bigButtonText}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
<TouchableOpacity onPress={() => setModalVisible(false)}>
|
||||||
<Text text70 marginT-20 center color="#999999" style={{fontFamily: 'Manrope_500Medium'}} >
|
<Text text70 marginT-20 center color="#999999" style={{fontFamily: 'Manrope_500Medium'}}>
|
||||||
Go back to my to dos
|
Go back to my to dos
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -183,7 +173,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
bigButtonText:{
|
bigButtonText: {
|
||||||
fontFamily: 'Manrope_400Regular'
|
fontFamily: 'Manrope_400Regular'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,9 +2,7 @@ import { useCreateNote } from "@/hooks/firebase/useCreateNote";
|
|||||||
import { useDeleteNote } from "@/hooks/firebase/useDeleteNote";
|
import { useDeleteNote } from "@/hooks/firebase/useDeleteNote";
|
||||||
import { useGetNotes } from "@/hooks/firebase/useGetNotes";
|
import { useGetNotes } from "@/hooks/firebase/useGetNotes";
|
||||||
import { useUpdateNote } from "@/hooks/firebase/useUpdateNote";
|
import { useUpdateNote } from "@/hooks/firebase/useUpdateNote";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
|
||||||
import { createContext, useContext, useState } from "react";
|
import { createContext, useContext, useState } from "react";
|
||||||
import { create } from "react-test-renderer";
|
|
||||||
|
|
||||||
export interface IBrainDump {
|
export interface IBrainDump {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { useCreateFeedback } from "@/hooks/firebase/useCreateFeedback";
|
import {useCreateFeedback} from "@/hooks/firebase/useCreateFeedback";
|
||||||
import { useDeleteFeedback } from "@/hooks/firebase/useDeleteFeedback";
|
import {useDeleteFeedback} from "@/hooks/firebase/useDeleteFeedback";
|
||||||
import { useGetFeedbacks } from "@/hooks/firebase/useGetFeedbacks";
|
import {useGetFeedbacks} from "@/hooks/firebase/useGetFeedbacks";
|
||||||
import { useUpdateFeedback } from "@/hooks/firebase/useUpdateFeedback";
|
import {useUpdateFeedback} from "@/hooks/firebase/useUpdateFeedback";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import {createContext, useContext, useState} from "react";
|
||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
|
|
||||||
export interface IFeedback {
|
export interface IFeedback {
|
||||||
id: number;
|
id: number;
|
||||||
@ -24,20 +23,18 @@ const FeedbackContext = createContext<IFeedbackContext | undefined>(undefined);
|
|||||||
|
|
||||||
export const FeedbackProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const FeedbackProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
mutateAsync: createFeedback,
|
mutateAsync: createFeedback,
|
||||||
isLoading: isAdding,
|
|
||||||
isError,
|
|
||||||
} = useCreateFeedback();
|
} = useCreateFeedback();
|
||||||
const { data: feedbacks } = useGetFeedbacks();
|
const {data: feedbacks} = useGetFeedbacks();
|
||||||
const { mutate: deleteFeedbackMutate } = useDeleteFeedback();
|
const {mutate: deleteFeedbackMutate} = useDeleteFeedback();
|
||||||
const { mutate: updateFeedbackMutate } = useUpdateFeedback();
|
const {mutate: updateFeedbackMutate} = useUpdateFeedback();
|
||||||
|
|
||||||
const [isAddingFeedback, setIsAddingFeedback] = useState<boolean>(false);
|
const [isAddingFeedback, setIsAddingFeedback] = useState<boolean>(false);
|
||||||
|
|
||||||
const addFeedback = (Feedback: IFeedback) => {
|
const addFeedback = (Feedback: IFeedback) => {
|
||||||
createFeedback({ title: Feedback.title, text: Feedback.text });
|
createFeedback({title: Feedback.title, text: Feedback.text});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFeedback = (id: number, changes: Partial<IFeedback>) => {
|
const updateFeedback = (id: number, changes: Partial<IFeedback>) => {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { View, Text } from "react-native";
|
|
||||||
import React, { createContext, useContext, useState } from "react";
|
import React, { createContext, useContext, useState } from "react";
|
||||||
export interface IReminder {
|
export interface IReminder {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -18,23 +18,190 @@ const GOOGLE_CALENDAR_ID = "primary";
|
|||||||
const CHANNEL_ID = "cally-family-calendar";
|
const CHANNEL_ID = "cally-family-calendar";
|
||||||
const WEBHOOK_URL = "https://us-central1-cally-family-calendar.cloudfunctions.net/sendSyncNotification";
|
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
|
exports.sendNotificationOnEventCreation = functions.firestore
|
||||||
.document('Events/{eventId}')
|
.document('Events/{eventId}')
|
||||||
.onCreate(async (snapshot, context) => {
|
.onCreate(async (snapshot, context) => {
|
||||||
const eventData = snapshot.data();
|
const eventData = snapshot.data();
|
||||||
const { familyId, creatorId, email, title } = eventData;
|
const {familyId, creatorId, email, title, externalOrigin} = eventData;
|
||||||
|
|
||||||
if (!!eventData?.externalOrigin) {
|
|
||||||
console.log('Externally synced event, ignoring.')
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!familyId || !creatorId) {
|
if (!familyId || !creatorId) {
|
||||||
console.error('Missing familyId or creatorId in event data');
|
console.error('Missing familyId or creatorId in event data');
|
||||||
return;
|
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) {
|
if (!pushTokens.length) {
|
||||||
console.log('No push tokens available for the event.');
|
console.log('No push tokens available for the event.');
|
||||||
@ -48,9 +215,16 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
notificationInProgress = true;
|
notificationInProgress = true;
|
||||||
|
|
||||||
notificationTimeout = setTimeout(async () => {
|
notificationTimeout = setTimeout(async () => {
|
||||||
const eventMessage = eventCount === 1
|
let eventMessage;
|
||||||
? `An event "${title}" has been added. Check it out!`
|
if (externalOrigin) {
|
||||||
: `${eventCount} new events have been added.`;
|
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 => {
|
let messages = pushTokens.map(pushToken => {
|
||||||
if (!Expo.isExpoPushToken(pushToken)) {
|
if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
@ -61,9 +235,12 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
return {
|
return {
|
||||||
to: pushToken,
|
to: pushToken,
|
||||||
sound: 'default',
|
sound: 'default',
|
||||||
title: 'New Events Added!',
|
title: externalOrigin ? 'Calendar Sync Complete' : 'New Family Calendar Events',
|
||||||
body: eventMessage,
|
body: eventMessage,
|
||||||
data: { eventId: context.params.eventId },
|
data: {
|
||||||
|
eventId: context.params.eventId,
|
||||||
|
type: externalOrigin ? 'sync' : 'manual'
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).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 = {
|
const notificationData = {
|
||||||
creatorId,
|
creatorId,
|
||||||
familyId,
|
familyId,
|
||||||
content: eventMessage,
|
content: eventMessage,
|
||||||
eventId: context.params.eventId,
|
eventId: context.params.eventId,
|
||||||
|
type: externalOrigin ? 'sync' : 'manual',
|
||||||
timestamp: Timestamp.now(),
|
timestamp: Timestamp.now(),
|
||||||
|
date: eventData.startDate
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -106,15 +285,159 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
console.error("Error saving notification to Firestore:", error);
|
console.error("Error saving notification to Firestore:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset state variables after notifications are sent
|
// Reset state variables
|
||||||
eventCount = 0;
|
eventCount = 0;
|
||||||
pushTokens = [];
|
pushTokens = [];
|
||||||
notificationInProgress = false;
|
notificationInProgress = false;
|
||||||
|
|
||||||
}, 5000);
|
}, 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) => {
|
exports.createSubUser = onRequest(async (request, response) => {
|
||||||
const authHeader = request.get('Authorization');
|
const authHeader = request.get('Authorization');
|
||||||
|
|
||||||
@ -209,7 +532,7 @@ exports.removeSubUser = onRequest(async (request, response) => {
|
|||||||
|
|
||||||
logger.info("Processing user removal", {requestBody: request.body.data});
|
logger.info("Processing user removal", {requestBody: request.body.data});
|
||||||
|
|
||||||
const { userId, familyId } = request.body.data;
|
const {userId, familyId} = request.body.data;
|
||||||
|
|
||||||
if (!userId || !familyId) {
|
if (!userId || !familyId) {
|
||||||
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
|
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) {
|
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");
|
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 baseDate = new Date();
|
||||||
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||||
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||||
@ -700,7 +1038,7 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
|||||||
token = refreshedToken;
|
token = refreshedToken;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
return fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId, creatorId });
|
return fetchAndSaveGoogleEvents({token, refreshToken, email, familyId, creatorId});
|
||||||
} else {
|
} else {
|
||||||
console.error(`Failed to refresh token for user: ${email}`);
|
console.error(`Failed to refresh token for user: ${email}`);
|
||||||
await clearToken(email);
|
await clearToken(email);
|
||||||
@ -717,8 +1055,12 @@ async function fetchAndSaveGoogleEvents({ token, refreshToken, email, familyId,
|
|||||||
const googleEvent = {
|
const googleEvent = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: item.summary || "",
|
title: item.summary || "",
|
||||||
startDate: item.start?.dateTime ? new Date(item.start.dateTime) : new Date(item.start.date),
|
startDate: item.start?.dateTime
|
||||||
endDate: item.end?.dateTime ? new Date(item.end.dateTime) : new Date(item.end.date),
|
? 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,
|
allDay: !item.start?.dateTime,
|
||||||
familyId,
|
familyId,
|
||||||
email,
|
email,
|
||||||
@ -743,12 +1085,12 @@ async function saveEventsToFirestore(events) {
|
|||||||
const batch = db.batch();
|
const batch = db.batch();
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
const eventRef = db.collection("Events").doc(event.id);
|
const eventRef = db.collection("Events").doc(event.id);
|
||||||
batch.set(eventRef, event, { merge: true });
|
batch.set(eventRef, event, {merge: true});
|
||||||
});
|
});
|
||||||
await batch.commit();
|
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}`);
|
console.log(`Starting calendar sync for user ${userId} with email ${email}`);
|
||||||
try {
|
try {
|
||||||
await fetchAndSaveGoogleEvents({
|
await fetchAndSaveGoogleEvents({
|
||||||
@ -787,7 +1129,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { googleAccounts } = userData;
|
const {googleAccounts} = userData;
|
||||||
const email = Object.keys(googleAccounts || {})[0];
|
const email = Object.keys(googleAccounts || {})[0];
|
||||||
const accountData = googleAccounts[email] || {};
|
const accountData = googleAccounts[email] || {};
|
||||||
const token = accountData.accessToken;
|
const token = accountData.accessToken;
|
||||||
@ -795,7 +1137,7 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
const familyId = userData.familyId;
|
const familyId = userData.familyId;
|
||||||
|
|
||||||
console.log("Starting calendar sync...");
|
console.log("Starting calendar sync...");
|
||||||
await calendarSync({ userId, email, token, refreshToken, familyId });
|
await calendarSync({userId, email, token, refreshToken, familyId});
|
||||||
console.log("Calendar sync completed.");
|
console.log("Calendar sync completed.");
|
||||||
|
|
||||||
res.status(200).send("Sync notification sent.");
|
res.status(200).send("Sync notification sent.");
|
||||||
@ -804,3 +1146,201 @@ exports.sendSyncNotification = functions.https.onRequest(async (req, res) => {
|
|||||||
res.status(500).send("Failed to send sync notification.");
|
res.status(500).send("Failed to send sync notification.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function refreshMicrosoftToken(refreshToken) {
|
||||||
|
try {
|
||||||
|
console.log("Refreshing Microsoft token...");
|
||||||
|
const response = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
||||||
|
scope: "openid profile email offline_access Calendars.ReadWrite User.Read",
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
grant_type: 'refresh_token'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Error refreshing Microsoft token:", errorData);
|
||||||
|
throw new Error(`Failed to refresh Microsoft token: ${errorData.error || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Microsoft token refreshed successfully");
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: data.access_token,
|
||||||
|
refreshToken: data.refresh_token || refreshToken
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Microsoft token:", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndSaveMicrosoftEvents({token, refreshToken, email, familyId, creatorId}) {
|
||||||
|
const baseDate = new Date();
|
||||||
|
const timeMin = new Date(baseDate.setMonth(baseDate.getMonth() - 1)).toISOString();
|
||||||
|
const timeMax = new Date(baseDate.setMonth(baseDate.getMonth() + 2)).toISOString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching Microsoft calendar events for user: ${email}`);
|
||||||
|
|
||||||
|
const url = `https://graph.microsoft.com/v1.0/me/calendar/events`;
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
$select: 'subject,start,end,id',
|
||||||
|
$filter: `start/dateTime ge '${timeMin}' and end/dateTime le '${timeMax}'`
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${url}?${queryParams}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 401 && refreshToken) {
|
||||||
|
console.log(`Token expired for user: ${email}, attempting to refresh`);
|
||||||
|
const {accessToken: newToken} = await refreshMicrosoftToken(refreshToken);
|
||||||
|
if (newToken) {
|
||||||
|
return fetchAndSaveMicrosoftEvents({
|
||||||
|
token: newToken,
|
||||||
|
refreshToken,
|
||||||
|
email,
|
||||||
|
familyId,
|
||||||
|
creatorId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching events: ${data.error?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = data.value.map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
title: item.subject || "",
|
||||||
|
startDate: new Date(item.start.dateTime + 'Z'),
|
||||||
|
endDate: new Date(item.end.dateTime + 'Z'),
|
||||||
|
allDay: false, // Microsoft Graph API handles all-day events differently
|
||||||
|
familyId,
|
||||||
|
email,
|
||||||
|
creatorId,
|
||||||
|
externalOrigin: "microsoft"
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(`Saving Microsoft calendar events to Firestore for user: ${email}`);
|
||||||
|
await saveEventsToFirestore(events);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching Microsoft Calendar events for ${email}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function subscribeMicrosoftCalendar(accessToken, userId) {
|
||||||
|
try {
|
||||||
|
console.log(`Setting up Microsoft calendar subscription for user ${userId}`);
|
||||||
|
|
||||||
|
const subscription = {
|
||||||
|
changeType: "created,updated,deleted",
|
||||||
|
notificationUrl: `https://us-central1-cally-family-calendar.cloudfunctions.net/microsoftCalendarWebhook?userId=${userId}`,
|
||||||
|
resource: "/me/calendar/events",
|
||||||
|
expirationDateTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days
|
||||||
|
clientState: userId
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('https://graph.microsoft.com/v1.0/subscriptions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(subscription)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to create subscription: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionData = await response.json();
|
||||||
|
|
||||||
|
// Store subscription details in Firestore
|
||||||
|
await admin.firestore().collection('MicrosoftSubscriptions').doc(userId).set({
|
||||||
|
subscriptionId: subscriptionData.id,
|
||||||
|
expirationDateTime: subscriptionData.expirationDateTime,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp()
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Microsoft calendar subscription created for user ${userId}`);
|
||||||
|
return subscriptionData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error creating Microsoft calendar subscription for user ${userId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.microsoftCalendarWebhook = functions.https.onRequest(async (req, res) => {
|
||||||
|
const userId = req.query.userId;
|
||||||
|
console.log(`Received Microsoft calendar webhook for user ${userId}`, req.body);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userDoc = await admin.firestore().collection("Profiles").doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
|
||||||
|
if (!userData?.microsoftAccounts) {
|
||||||
|
console.log(`No Microsoft account found for user ${userId}`);
|
||||||
|
return res.status(200).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||||
|
const token = userData.microsoftAccounts[email];
|
||||||
|
|
||||||
|
await fetchAndSaveMicrosoftEvents({
|
||||||
|
token,
|
||||||
|
email,
|
||||||
|
familyId: userData.familyId,
|
||||||
|
creatorId: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing Microsoft calendar webhook for user ${userId}:`, error);
|
||||||
|
res.status(500).send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
exports.renewMicrosoftSubscriptions = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
|
||||||
|
console.log('Starting Microsoft subscription renewal process');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscriptionsSnapshot = await admin.firestore()
|
||||||
|
.collection('MicrosoftSubscriptions')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
for (const doc of subscriptionsSnapshot.docs) {
|
||||||
|
const userId = doc.id;
|
||||||
|
const userDoc = await admin.firestore().collection('Profiles').doc(userId).get();
|
||||||
|
const userData = userDoc.data();
|
||||||
|
|
||||||
|
if (userData?.microsoftAccounts) {
|
||||||
|
const email = Object.keys(userData.microsoftAccounts)[0];
|
||||||
|
const token = userData.microsoftAccounts[email];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await subscribeMicrosoftCalendar(token, userId);
|
||||||
|
console.log(`Renewed Microsoft subscription for user ${userId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to renew Microsoft subscription for user ${userId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in Microsoft subscription renewal process:', error);
|
||||||
|
}
|
||||||
|
});
|
@ -1,13 +1,13 @@
|
|||||||
import { useQuery } from "react-query";
|
import {useQuery} from "react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import { useAtomValue } from "jotai";
|
import {useAtomValue} from "jotai";
|
||||||
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
|
|
||||||
export const useGetEvents = () => {
|
export const useGetEvents = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const {user, profileData} = useAuthContext();
|
||||||
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
const isFamilyView = useAtomValue(isFamilyViewAtom);
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@ -19,62 +19,61 @@ export const useGetEvents = () => {
|
|||||||
|
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
|
|
||||||
// If family view is active, include family, creator, and attendee events
|
|
||||||
if (isFamilyView) {
|
if (isFamilyView) {
|
||||||
const familyQuery = db.collection("Events").where("familyId", "==", familyId);
|
// Get public family events
|
||||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
const publicFamilyEvents = await db.collection("Events")
|
||||||
|
.where("familyId", "==", familyId)
|
||||||
|
.where("private", "==", false)
|
||||||
|
.get();
|
||||||
|
|
||||||
const [familySnapshot, attendeeSnapshot] = await Promise.all([
|
// Get private events where user is creator
|
||||||
familyQuery.get(),
|
const privateCreatorEvents = await db.collection("Events")
|
||||||
attendeeQuery.get(),
|
.where("familyId", "==", familyId)
|
||||||
]);
|
.where("private", "==", true)
|
||||||
|
.where("creatorId", "==", userId)
|
||||||
|
.get();
|
||||||
|
|
||||||
// Collect all events
|
// Get private events where user is attendee
|
||||||
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
const privateAttendeeEvents = await db.collection("Events")
|
||||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
.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 = [
|
||||||
|
...publicFamilyEvents.docs.map(doc => doc.data()),
|
||||||
|
...privateCreatorEvents.docs.map(doc => doc.data()),
|
||||||
allEvents = [...familyEvents, ...attendeeEvents];
|
...privateAttendeeEvents.docs.map(doc => doc.data())
|
||||||
|
];
|
||||||
} else {
|
} else {
|
||||||
// Only include creator and attendee events when family view is off
|
// Personal view: Only show events where user is creator or attendee
|
||||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
const [creatorEvents, attendeeEvents] = await Promise.all([
|
||||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
db.collection("Events")
|
||||||
|
.where("creatorId", "==", userId)
|
||||||
const [creatorSnapshot, attendeeSnapshot] = await Promise.all([
|
.get(),
|
||||||
creatorQuery.get(),
|
db.collection("Events")
|
||||||
attendeeQuery.get(),
|
.where("attendees", "array-contains", userId)
|
||||||
|
.get()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
|
allEvents = [
|
||||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
...creatorEvents.docs.map(doc => doc.data()),
|
||||||
|
...attendeeEvents.docs.map(doc => doc.data())
|
||||||
allEvents = [...creatorEvents, ...attendeeEvents];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a Map to ensure uniqueness only for events with IDs
|
// Ensure uniqueness
|
||||||
const uniqueEventsMap = new Map();
|
const uniqueEventsMap = new Map();
|
||||||
allEvents.forEach(event => {
|
allEvents.forEach(event => {
|
||||||
if (event.id) {
|
if (event.id) {
|
||||||
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
|
uniqueEventsMap.set(event.id, event);
|
||||||
} else {
|
} 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
|
// Map events with creator colors
|
||||||
const filteredEvents = uniqueEvents.filter(event => {
|
|
||||||
if (event.private) {
|
|
||||||
return event.creatorId === userId;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach event colors and return the final list of events
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
filteredEvents.map(async (event) => {
|
Array.from(uniqueEventsMap.values()).map(async (event) => {
|
||||||
const profileSnapshot = await db
|
const profileSnapshot = await db
|
||||||
.collection("Profiles")
|
.collection("Profiles")
|
||||||
.doc(event.creatorId)
|
.doc(event.creatorId)
|
||||||
@ -85,10 +84,12 @@ export const useGetEvents = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...event,
|
...event,
|
||||||
id: event.id || Math.random().toString(36).slice(2, 9), // Generate temp ID if missing
|
start: event.allDay
|
||||||
title: event.title,
|
? new Date(new Date(event.startDate.seconds * 1000).setHours(0, 0, 0, 0))
|
||||||
start: new Date(event.startDate.seconds * 1000),
|
: new Date(event.startDate.seconds * 1000),
|
||||||
end: new Date(event.endDate.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,
|
hideHours: event.allDay,
|
||||||
eventColor,
|
eventColor,
|
||||||
notes: event.notes,
|
notes: event.notes,
|
||||||
|
@ -1,11 +1,34 @@
|
|||||||
import {useQuery} from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import 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 = () => {
|
export const useGetNotifications = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
|
|
||||||
return useQuery({
|
return useQuery<Notification[], Error>({
|
||||||
queryKey: ["notifications", user?.uid],
|
queryKey: ["notifications", user?.uid],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const snapshot = await firestore()
|
const snapshot = await firestore()
|
||||||
@ -14,16 +37,14 @@ export const useGetNotifications = () => {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
return snapshot.docs.map((doc) => {
|
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 {
|
return {
|
||||||
creatorId: string,
|
...data,
|
||||||
familyId: string,
|
timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6),
|
||||||
content: string,
|
date: data.date ? new Date(data.date.seconds * 1000 + data.date.nanoseconds / 1e6) : undefined
|
||||||
eventId: string,
|
|
||||||
timestamp: Date,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
};
|
};
|
@ -48,6 +48,8 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>74</string>
|
<string>74</string>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<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>
|
||||||
<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>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
Reference in New Issue
Block a user