mirror of
https://github.com/urosran/cally.git
synced 2025-11-26 08:24:55 +00:00
Merge branch 'dev' into tablet
This commit is contained in:
2
app.json
2
app.json
@ -16,7 +16,7 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "com.cally.app",
|
"bundleIdentifier": "com.cally.app",
|
||||||
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
"googleServicesFile": "./ios/GoogleService-Info.plist",
|
||||||
"buildNumber": "40",
|
"buildNumber": "60",
|
||||||
"usesAppleSignIn": true
|
"usesAppleSignIn": true
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import {
|
|||||||
DrawerItem,
|
DrawerItem,
|
||||||
DrawerItemList,
|
DrawerItemList,
|
||||||
} from "@react-navigation/drawer";
|
} from "@react-navigation/drawer";
|
||||||
import { Button, View, Text, ButtonSize, Constants } from "react-native-ui-lib";
|
import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
|
||||||
import { StyleSheet } from "react-native";
|
import { Dimensions, ImageBackground, StyleSheet } from "react-native";
|
||||||
import Feather from "@expo/vector-icons/Feather";
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
import DrawerButton from "@/components/shared/DrawerButton";
|
import DrawerButton from "@/components/shared/DrawerButton";
|
||||||
import {
|
import {
|
||||||
@ -24,9 +24,21 @@ 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 { useAtom, useSetAtom } from "jotai";
|
||||||
|
import {
|
||||||
|
isFamilyViewAtom,
|
||||||
|
settingsPageIndex,
|
||||||
|
toDosPageIndex,
|
||||||
|
userSettingsView,
|
||||||
|
} from "@/components/pages/calendar/atoms";
|
||||||
|
import FeedbackNavIcon from "@/assets/svgs/FeedbackNavIcon";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
const { mutateAsync: signOut } = useSignOut();
|
const { mutateAsync: signOut } = useSignOut();
|
||||||
|
const setIsFamilyView = useSetAtom(isFamilyViewAtom);
|
||||||
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
|
const setUserView = useSetAtom(userSettingsView);
|
||||||
|
const setToDosIndex = useSetAtom(toDosPageIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
@ -45,12 +57,20 @@ export default function TabLayout() {
|
|||||||
backgroundColor: "#f9f8f7",
|
backgroundColor: "#f9f8f7",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
drawerIcon: () => <MenuIcon />,
|
|
||||||
}}
|
}}
|
||||||
drawerContent={(props) => {
|
drawerContent={(props) => {
|
||||||
return (
|
return (
|
||||||
<DrawerContentScrollView {...props} style={{ height: "100%" }}>
|
<DrawerContentScrollView {...props} style={{}}>
|
||||||
<View centerH centerV margin-30>
|
<View centerV marginH-30 marginT-20 marginB-20 row>
|
||||||
|
<ImageBackground
|
||||||
|
source={require("../../assets/images/splash.png")}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
height: 51.43,
|
||||||
|
aspectRatio: 1,
|
||||||
|
marginRight: 8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Text style={styles.title}>Welcome to Cally</Text>
|
<Text style={styles.title}>Welcome to Cally</Text>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
@ -64,16 +84,41 @@ export default function TabLayout() {
|
|||||||
title={"Calendar"}
|
title={"Calendar"}
|
||||||
color="rgb(7, 184, 199)"
|
color="rgb(7, 184, 199)"
|
||||||
bgColor={"rgb(231, 248, 250)"}
|
bgColor={"rgb(231, 248, 250)"}
|
||||||
pressFunc={() => props.navigation.navigate("calendar")}
|
pressFunc={() => {
|
||||||
|
props.navigation.navigate("calendar");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
icon={<NavCalendarIcon />}
|
icon={<NavCalendarIcon />}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#50be0c"
|
color="#50be0c"
|
||||||
title={"Groceries"}
|
title={"Groceries"}
|
||||||
bgColor={"#eef9e7"}
|
bgColor={"#eef9e7"}
|
||||||
pressFunc={() => props.navigation.navigate("grocery")}
|
pressFunc={() => {
|
||||||
|
props.navigation.navigate("grocery");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
icon={<NavGroceryIcon />}
|
icon={<NavGroceryIcon />}
|
||||||
/>
|
/>
|
||||||
|
<DrawerButton
|
||||||
|
color="#ea156d"
|
||||||
|
title={"Feedback"}
|
||||||
|
bgColor={"#fdedf4"}
|
||||||
|
pressFunc={() => {
|
||||||
|
props.navigation.navigate("feedback");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
|
icon={<FeedbackNavIcon />}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
{/*<DrawerButton
|
{/*<DrawerButton
|
||||||
@ -93,21 +138,39 @@ export default function TabLayout() {
|
|||||||
color="#8005eb"
|
color="#8005eb"
|
||||||
title={"To Do's"}
|
title={"To Do's"}
|
||||||
bgColor={"#f3e6fd"}
|
bgColor={"#f3e6fd"}
|
||||||
pressFunc={() => props.navigation.navigate("todos")}
|
pressFunc={() => {
|
||||||
|
props.navigation.navigate("todos");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
icon={<NavToDosIcon />}
|
icon={<NavToDosIcon />}
|
||||||
/>
|
/>
|
||||||
<DrawerButton
|
<DrawerButton
|
||||||
color="#e0ca03"
|
color="#e0ca03"
|
||||||
title={"Brain Dump"}
|
title={"Brain Dump"}
|
||||||
bgColor={"#fffacb"}
|
bgColor={"#fffacb"}
|
||||||
pressFunc={() => props.navigation.navigate("brain_dump")}
|
pressFunc={() => {
|
||||||
|
props.navigation.navigate("brain_dump");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
icon={<NavBrainDumpIcon />}
|
icon={<NavBrainDumpIcon />}
|
||||||
/>
|
/>
|
||||||
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
|
{/*<DrawerItem label="Logout" onPress={() => signOut()} />*/}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => props.navigation.navigate("settings")}
|
onPress={() => {
|
||||||
|
props.navigation.navigate("settings");
|
||||||
|
setPageIndex(0);
|
||||||
|
setToDosIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
setIsFamilyView(false);
|
||||||
|
}}
|
||||||
label={"Manage Settings"}
|
label={"Manage Settings"}
|
||||||
labelStyle={styles.label}
|
labelStyle={styles.label}
|
||||||
iconSource={() => (
|
iconSource={() => (
|
||||||
@ -135,9 +198,9 @@ export default function TabLayout() {
|
|||||||
<Button
|
<Button
|
||||||
size={ButtonSize.large}
|
size={ButtonSize.large}
|
||||||
marginH-30
|
marginH-30
|
||||||
|
marginT-12
|
||||||
paddingV-15
|
paddingV-15
|
||||||
style={{
|
style={{
|
||||||
marginTop: "42%",
|
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
borderWidth: 1.3,
|
borderWidth: 1.3,
|
||||||
borderColor: "#fd1775",
|
borderColor: "#fd1775",
|
||||||
@ -198,9 +261,13 @@ export default function TabLayout() {
|
|||||||
name="todos"
|
name="todos"
|
||||||
options={{
|
options={{
|
||||||
drawerLabel: "To-Do",
|
drawerLabel: "To-Do",
|
||||||
title: "To-Do's",
|
title: "To-Dos",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Drawer.Screen
|
||||||
|
name="feedback"
|
||||||
|
options={{ drawerLabel: "Feedback", title: "Feedback" }}
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
5
app/(auth)/feedback/_layout.tsx
Normal file
5
app/(auth)/feedback/_layout.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import {Stack} from "expo-router";
|
||||||
|
|
||||||
|
export default function StackLayout () {
|
||||||
|
return <Stack screenOptions={{headerShown: false}}/>
|
||||||
|
}
|
||||||
13
app/(auth)/feedback/index.tsx
Normal file
13
app/(auth)/feedback/index.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import FeedbackPage from "@/components/pages/feedback/FeedbackPage";
|
||||||
|
import { FeedbackProvider } from "@/contexts/FeedbackContext";
|
||||||
|
import { View } from "react-native-ui-lib";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return (
|
||||||
|
<FeedbackProvider>
|
||||||
|
<View>
|
||||||
|
<FeedbackPage />
|
||||||
|
</View>
|
||||||
|
</FeedbackProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
app/(unauth)/cal_sync.tsx
Normal file
246
app/(unauth)/cal_sync.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {Button, Text, View} from "react-native-ui-lib";
|
||||||
|
import React from "react";
|
||||||
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
|
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||||
|
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||||
|
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {StyleSheet} from "react-native";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
const {profileData, setRedirectOverride} = useAuthContext()
|
||||||
|
const {handleStartGoogleSignIn, handleAppleSignIn, handleMicrosoftSignIn} = useCalSync()
|
||||||
|
|
||||||
|
const hasSomeCalendarsSynced =
|
||||||
|
!!profileData?.appleAccounts || !!profileData?.microsoftAccounts || !!profileData?.googleAccounts
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{flex: 1}}>
|
||||||
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||||
|
<View gap-13 width={"100%"} marginB-20>
|
||||||
|
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}>
|
||||||
|
Let's get started!
|
||||||
|
</Text>
|
||||||
|
<Text color={"#919191"} style={{fontSize: 20}}>
|
||||||
|
Add your calendar below to sync events to your Cally calendar
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View width={"100%"} gap-1>
|
||||||
|
{!profileData?.googleAccounts && (
|
||||||
|
<Button
|
||||||
|
onPress={() => handleStartGoogleSignIn()}
|
||||||
|
label={"Connect Google account"}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<GoogleIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{profileData?.googleAccounts
|
||||||
|
? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
|
||||||
|
const googleToken = profileData?.googleAccounts?.[googleMail]?.accessToken;
|
||||||
|
return (
|
||||||
|
googleToken && (
|
||||||
|
<Button
|
||||||
|
key={googleMail}
|
||||||
|
disabled
|
||||||
|
label={`Connected ${googleMail}`}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<GoogleIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{!profileData?.appleAccounts && (
|
||||||
|
<Button
|
||||||
|
onPress={() => handleAppleSignIn()}
|
||||||
|
label={"Connect Apple"}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<AppleIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{profileData?.appleAccounts
|
||||||
|
? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
|
||||||
|
const appleToken = profileData?.appleAccounts?.[appleEmail!];
|
||||||
|
return (
|
||||||
|
appleToken && (
|
||||||
|
<Button
|
||||||
|
key={appleEmail}
|
||||||
|
disabled
|
||||||
|
label={`Connected Apple Calendar`}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<AppleIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
|
||||||
|
{!profileData?.microsoftAccounts && (
|
||||||
|
<Button
|
||||||
|
onPress={() => handleMicrosoftSignIn()}
|
||||||
|
label={"Connect Outlook"}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<OutlookIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{profileData?.microsoftAccounts
|
||||||
|
? Object.keys(profileData?.microsoftAccounts)?.map(
|
||||||
|
(microsoftEmail) => {
|
||||||
|
const microsoftToken =
|
||||||
|
profileData?.microsoftAccounts?.[microsoftEmail];
|
||||||
|
return (
|
||||||
|
microsoftToken && (
|
||||||
|
<Button
|
||||||
|
key={microsoftEmail}
|
||||||
|
|
||||||
|
label={`Connected ${microsoftEmail}`}
|
||||||
|
labelStyle={styles.addCalLbl}
|
||||||
|
labelProps={{
|
||||||
|
numberOfLines: 2,
|
||||||
|
}}
|
||||||
|
iconSource={() => (
|
||||||
|
<View marginR-15>
|
||||||
|
<OutlookIcon/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
style={styles.addCalBtn}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View flexG/>
|
||||||
|
|
||||||
|
<View width={"100%"}>
|
||||||
|
<Button
|
||||||
|
label={hasSomeCalendarsSynced ? "Continue" : "Skip this step"}
|
||||||
|
onPress={() => setRedirectOverride(false)}
|
||||||
|
marginT-50
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
style={{height: 50}}
|
||||||
|
backgroundColor="#fd1775"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
addCalBtn: {
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
marginBottom: 15,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
paddingLeft: 25,
|
||||||
|
},
|
||||||
|
backBtn: {
|
||||||
|
backgroundColor: "red",
|
||||||
|
marginLeft: -2,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "100%",
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 30,
|
||||||
|
marginTop: 20,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
noPaddingCard: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "100%",
|
||||||
|
marginTop: 20,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
colorBox: {
|
||||||
|
aspectRatio: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: 51,
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
checkbox: {
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
addCalLbl: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: "PlusJakartaSan_500Medium",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
width: "75%",
|
||||||
|
textAlign: "left",
|
||||||
|
lineHeight: 20,
|
||||||
|
overflow: "visible",
|
||||||
|
},
|
||||||
|
subTitle: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
cardTitle: {
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
169
app/(unauth)/get_started.tsx
Normal file
169
app/(unauth)/get_started.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {Button, Colors, Dialog, LoaderScreen, Text, View} from "react-native-ui-lib";
|
||||||
|
import React, {useCallback, useState} from "react";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
import QRIcon from "@/assets/svgs/QRIcon";
|
||||||
|
import {Camera, CameraView} from "expo-camera";
|
||||||
|
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import debounce from "debounce";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
const router = useRouter()
|
||||||
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||||
|
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const {mutateAsync: signInWithQrCode, isLoading} = useLoginWithQrCode();
|
||||||
|
|
||||||
|
const debouncedRouterReplace = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
router.push("/(unauth)/cal_sync");
|
||||||
|
}, 300),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQrCodeScanned = async ({data}: { data: string }) => {
|
||||||
|
setShowCameraDialog(false);
|
||||||
|
setRedirectOverride(true);
|
||||||
|
try {
|
||||||
|
await signInWithQrCode({userId: data});
|
||||||
|
debouncedRouterReplace()
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCameraPermissions = async (callback: () => void) => {
|
||||||
|
const {status} = await Camera.requestCameraPermissionsAsync();
|
||||||
|
setHasPermission(status === "granted");
|
||||||
|
if (status === "granted") {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenQrCodeDialog = () => {
|
||||||
|
getCameraPermissions(() => setShowCameraDialog(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{flex: 1}}>
|
||||||
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||||
|
<View center>
|
||||||
|
<Text style={{fontSize: 30, fontFamily: 'Manrope_600SemiBold'}}>
|
||||||
|
Get started with Cally
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View width={"100%"} gap-30>
|
||||||
|
<View>
|
||||||
|
<Button
|
||||||
|
label="Scan QR Code"
|
||||||
|
marginT-50
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
iconSource={() => <QRIcon color={"#07B8C7"}/>}
|
||||||
|
onPress={handleOpenQrCodeDialog}
|
||||||
|
style={{height: 50}}
|
||||||
|
color={Colors.black}
|
||||||
|
backgroundColor={Colors.white}
|
||||||
|
/>
|
||||||
|
{/* GOOGLE LOGIN HERE */}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View row center gap-20>
|
||||||
|
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
|
||||||
|
<Text style={{fontSize: 16, fontFamily: 'PlusJakartaSans_300Light', color: "#7A7A7A"}}>
|
||||||
|
or
|
||||||
|
</Text>
|
||||||
|
<View flexG style={{backgroundColor: "#E2E2E2", height: 2}}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Button
|
||||||
|
label="Contine with Email"
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginLeft: 10
|
||||||
|
}}
|
||||||
|
onPress={() => router.push("/(unauth)/sign_up")}
|
||||||
|
style={{height: 50, borderStyle: "solid", borderColor: "#E2E2E2", borderWidth: 2}}
|
||||||
|
color={Colors.black}
|
||||||
|
backgroundColor={"transparent"}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
<View flexG/>
|
||||||
|
|
||||||
|
<View row centerH gap-5>
|
||||||
|
<Text style={{
|
||||||
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#484848"
|
||||||
|
}} center>
|
||||||
|
Already have an account?
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label="Log in"
|
||||||
|
link
|
||||||
|
onPress={() => router.push("/(unauth)/sign_in")}
|
||||||
|
labelStyle={[
|
||||||
|
{
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#919191",
|
||||||
|
},
|
||||||
|
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Legacy, move into separate component */}
|
||||||
|
{/* Camera Dialog */}
|
||||||
|
<Dialog
|
||||||
|
visible={showCameraDialog}
|
||||||
|
onDismiss={() => setShowCameraDialog(false)}
|
||||||
|
bottom
|
||||||
|
width="100%"
|
||||||
|
height="70%"
|
||||||
|
containerStyle={{padding: 15, backgroundColor: "white"}}
|
||||||
|
>
|
||||||
|
<Text center style={{fontSize: 16}} marginB-15>
|
||||||
|
Scan a QR code presented from your family member of provider.
|
||||||
|
</Text>
|
||||||
|
{hasPermission === null ? (
|
||||||
|
<Text>Requesting camera permissions...</Text>
|
||||||
|
) : !hasPermission ? (
|
||||||
|
<Text>No access to camera</Text>
|
||||||
|
) : (
|
||||||
|
<CameraView
|
||||||
|
style={{flex: 1, borderRadius: 15}}
|
||||||
|
onBarcodeScanned={handleQrCodeScanned}
|
||||||
|
barcodeScannerSettings={{
|
||||||
|
barcodeTypes: ["qr"],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={() => setShowCameraDialog(false)}
|
||||||
|
backgroundColor="#fd1775"
|
||||||
|
style={{margin: 10, marginBottom: 30}}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} color={Colors.grey40}/>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,10 +1,44 @@
|
|||||||
import Entry from "@/components/pages/main/Entry";
|
|
||||||
import {SafeAreaView} from "react-native-safe-area-context";
|
import {SafeAreaView} from "react-native-safe-area-context";
|
||||||
|
import {Button, Image, Text, View} from "react-native-ui-lib";
|
||||||
|
import React from "react";
|
||||||
|
import {useRouter} from "expo-router";
|
||||||
|
|
||||||
export default function Screen() {
|
export default function Screen() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<Entry/>
|
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%", alignItems: "center"}}>
|
||||||
|
<View>
|
||||||
|
<Image source={require("../../assets/images/splash.png")}/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View center gap-13>
|
||||||
|
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||||
|
Welcome to Cally
|
||||||
|
</Text>
|
||||||
|
<Text center color={"#919191"} style={{fontSize: 20, maxWidth: 250}}>
|
||||||
|
Lightening Mental Loads,
|
||||||
|
One Family at a Time
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View flexG/>
|
||||||
|
|
||||||
|
<View width={"100%"}>
|
||||||
|
<Button
|
||||||
|
label="Continue"
|
||||||
|
marginT-50
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
onPress={() => router.push("/(unauth)/get_started")}
|
||||||
|
style={{height: 50}}
|
||||||
|
backgroundColor="#fd1775"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
6
app/(unauth)/reset_password.tsx
Normal file
6
app/(unauth)/reset_password.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {ResetPasswordPage} from "@/components/pages/main/ResetPasswordPage";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return <ResetPasswordPage/>
|
||||||
|
}
|
||||||
6
app/(unauth)/sign_in.tsx
Normal file
6
app/(unauth)/sign_in.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import SignInPage from "@/components/pages/main/SignInPage";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return <SignInPage/>
|
||||||
|
}
|
||||||
8
app/(unauth)/sign_up.tsx
Normal file
8
app/(unauth)/sign_up.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import SignUpPage from "@/components/pages/main/SignUpPage";
|
||||||
|
|
||||||
|
export default function Screen() {
|
||||||
|
return (
|
||||||
|
<SignUpPage/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -5,13 +5,14 @@ const CloseXIcon: React.FC<SvgProps> = (props) => (
|
|||||||
width={15}
|
width={15}
|
||||||
height={15}
|
height={15}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
viewBox="0 0 15 15"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Path
|
<Path
|
||||||
stroke="#AAA"
|
stroke="#AAA"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={1.394}
|
strokeWidth={props.strokeWidth || 1.394}
|
||||||
d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543"
|
d="m1.573 1.543 12.544 12.544M1.573 14.087 14.117 1.543"
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
|
|||||||
20
assets/svgs/FeedbackNavIcon.tsx
Normal file
20
assets/svgs/FeedbackNavIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import Svg, { SvgProps, Path } from "react-native-svg"
|
||||||
|
const FeedbackNavIcon = (props: SvgProps) => (
|
||||||
|
<Svg
|
||||||
|
width={25}
|
||||||
|
height={25}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
stroke="#ea156d"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
d="M10.5 21H4a7.001 7.001 0 0 1 6-6.93m6.498 2.142c-.7-.78-1.867-.989-2.744-.275-.877.713-1 1.906-.311 2.75.388.476 1.312 1.311 2.042 1.948.347.302.52.453.73.515.178.053.387.053.566 0 .21-.061.382-.213.729-.515.73-.637 1.654-1.472 2.043-1.948.688-.844.58-2.044-.312-2.75-.892-.706-2.044-.504-2.743.275ZM15 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
export default FeedbackNavIcon
|
||||||
@ -3,8 +3,8 @@ import Svg, { Path, LinearGradient, Stop, SvgProps } from "react-native-svg";
|
|||||||
|
|
||||||
const OutlookIcon: React.FC<SvgProps> = (props) => (
|
const OutlookIcon: React.FC<SvgProps> = (props) => (
|
||||||
<Svg
|
<Svg
|
||||||
width={34}
|
width={props.width || 34}
|
||||||
height={34}
|
height={props.height || 34}
|
||||||
viewBox="0 0 48 48"
|
viewBox="0 0 48 48"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
20
assets/svgs/PlusIcon.tsx
Normal file
20
assets/svgs/PlusIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Svg, { SvgProps, Path } from "react-native-svg";
|
||||||
|
const PlusIcon = (props: SvgProps) => (
|
||||||
|
<Svg
|
||||||
|
width={props.width || 14}
|
||||||
|
height={props.height || 15}
|
||||||
|
viewBox="0 0 14 15"
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
stroke="#fff"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M1 7.632h12m-6-6v12"
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
);
|
||||||
|
export default PlusIcon;
|
||||||
@ -2,8 +2,9 @@ import * as Calendar from 'expo-calendar';
|
|||||||
|
|
||||||
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
|
export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endDate) {
|
||||||
try {
|
try {
|
||||||
const {status} = await Calendar.requestCalendarPermissionsAsync();
|
const {granted} = await Calendar.requestCalendarPermissionsAsync();
|
||||||
if (status !== 'granted') {
|
|
||||||
|
if (!granted) {
|
||||||
throw new Error("Calendar permission not granted");
|
throw new Error("Calendar permission not granted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +23,11 @@ export async function fetchiPhoneCalendarEvents(familyId, email, startDate, endD
|
|||||||
return events.map((event) => {
|
return events.map((event) => {
|
||||||
let isAllDay = event.allDay || false;
|
let isAllDay = event.allDay || false;
|
||||||
const startDateTime = new Date(event.startDate);
|
const startDateTime = new Date(event.startDate);
|
||||||
const endDateTime = new Date(event.endDate);
|
let endDateTime = new Date(event.endDate);
|
||||||
|
|
||||||
|
if (isAllDay) {
|
||||||
|
endDateTime = startDateTime
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
|
|||||||
@ -8,7 +8,9 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const googleEvents = [];
|
const googleEvents = [];
|
||||||
data.items?.forEach((item) => {
|
data.items?.forEach((item) => {
|
||||||
let isAllDay = false;
|
let isAllDay = false;
|
||||||
@ -49,5 +51,5 @@ export async function fetchGoogleCalendarEvents(token, email, familyId, startDat
|
|||||||
googleEvents.push(googleEvent);
|
googleEvents.push(googleEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
return googleEvents;
|
return {googleEvents, success: response.ok};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
import {
|
import {Button, Dialog, TextField, TextFieldRef, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
View,
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
Text,
|
import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
|
||||||
Button,
|
import {Dimensions, Platform, StyleSheet} from "react-native";
|
||||||
TextField,
|
|
||||||
TextFieldRef,
|
|
||||||
TouchableOpacity,
|
|
||||||
} from "react-native-ui-lib";
|
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
|
||||||
import { Dialog } from "react-native-ui-lib";
|
|
||||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
|
||||||
import { Dimensions, Keyboard, StyleSheet } from "react-native";
|
|
||||||
|
|
||||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
import {useBrainDumpContext} from "@/contexts/DumpContext";
|
||||||
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
|
||||||
interface IAddBrainDumpProps {
|
interface IAddBrainDumpProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
@ -21,22 +14,36 @@ interface IAddBrainDumpProps {
|
|||||||
|
|
||||||
const AddBrainDump = ({
|
const AddBrainDump = ({
|
||||||
addBrainDumpProps,
|
addBrainDumpProps,
|
||||||
}: {
|
}: {
|
||||||
addBrainDumpProps: IAddBrainDumpProps;
|
addBrainDumpProps: IAddBrainDumpProps;
|
||||||
}) => {
|
}) => {
|
||||||
const { addBrainDump } = useBrainDumpContext();
|
const {addBrainDump} = useBrainDumpContext();
|
||||||
const [dumpTitle, setDumpTitle] = useState<string>("");
|
const [dumpTitle, setDumpTitle] = useState<string>("");
|
||||||
const [dumpDesc, setDumpDesc] = useState<string>("");
|
const [dumpDesc, setDumpDesc] = useState<string>("");
|
||||||
const { width } = Dimensions.get("screen");
|
const {width} = Dimensions.get("screen");
|
||||||
|
|
||||||
|
|
||||||
// Refs for the two TextFields
|
// Refs for the two TextFields
|
||||||
const descriptionRef = useRef<TextFieldRef>(null);
|
const descriptionRef = useRef<TextFieldRef>(null);
|
||||||
|
const titleRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDumpDesc("");
|
setDumpDesc("");
|
||||||
setDumpTitle("");
|
setDumpTitle("");
|
||||||
}, [addBrainDumpProps.isVisible]);
|
}, [addBrainDumpProps.isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addBrainDumpProps.isVisible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}, [addBrainDumpProps.isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
bottom={true}
|
bottom={true}
|
||||||
@ -57,14 +64,22 @@ const AddBrainDump = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
<TouchableOpacity onPress={() => addBrainDumpProps.setIsVisible(false)}>
|
||||||
<DropModalIcon style={{ marginTop: 15 }} />
|
<DropModalIcon style={{marginTop: 15}}/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Button
|
<Button
|
||||||
color="#05a8b6"
|
color="#05a8b6"
|
||||||
label="Save"
|
label="Save"
|
||||||
style={styles.topBtn}
|
style={styles.topBtn}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
addBrainDump({ id: 99, title: dumpTitle.trimEnd().trimStart(), description: dumpDesc.trimEnd().trimStart() });
|
addBrainDump({
|
||||||
|
|
||||||
|
id: 99,
|
||||||
|
|
||||||
|
title: dumpTitle.trimEnd().trimStart(),
|
||||||
|
|
||||||
|
description: dumpDesc.trimEnd().trimStart(),
|
||||||
|
|
||||||
|
});
|
||||||
addBrainDumpProps.setIsVisible(false);
|
addBrainDumpProps.setIsVisible(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -72,7 +87,7 @@ const AddBrainDump = ({
|
|||||||
<View marginH-20>
|
<View marginH-20>
|
||||||
<TextField
|
<TextField
|
||||||
value={dumpTitle}
|
value={dumpTitle}
|
||||||
autoFocus
|
ref={titleRef}
|
||||||
placeholder="Set Title"
|
placeholder="Set Title"
|
||||||
text60R
|
text60R
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
@ -86,7 +101,7 @@ const AddBrainDump = ({
|
|||||||
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
blurOnSubmit={false} // Keep the keyboard open when moving focus
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
|
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20/>
|
||||||
<TextField
|
<TextField
|
||||||
ref={descriptionRef}
|
ref={descriptionRef}
|
||||||
value={dumpDesc}
|
value={dumpDesc}
|
||||||
@ -130,7 +145,7 @@ const styles = StyleSheet.create({
|
|||||||
description: {
|
description: {
|
||||||
fontFamily: "Manrope_400Regular",
|
fontFamily: "Manrope_400Regular",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
textAlignVertical: 'top'
|
textAlignVertical: "top",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
82
components/pages/brain_dump/BrainDumpDialog.tsx
Normal file
82
components/pages/brain_dump/BrainDumpDialog.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
interface BrainDumpDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BrainDumpDialog: React.FC<BrainDumpDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
onDismiss,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<Text center style={styles.title}>
|
||||||
|
Delete Note
|
||||||
|
</Text>
|
||||||
|
<View center>
|
||||||
|
<Text style={styles.text} center>
|
||||||
|
Are you sure you want to delete the {"\n"}
|
||||||
|
<Text style={{ fontSize: 16, fontFamily: "PlusJakartaSans_700Bold" }}>
|
||||||
|
{title}
|
||||||
|
</Text>{" "}
|
||||||
|
Note?
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View row right gap-8>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={onDismiss}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={onConfirm}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stylesheet for future styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
confirmBtn: {
|
||||||
|
backgroundColor: "#ea156d",
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
paddingTop: 35,
|
||||||
|
paddingBottom: 17,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BrainDumpDialog;
|
||||||
@ -1,14 +1,12 @@
|
|||||||
import { Dimensions, ScrollView } from "react-native";
|
import {Dimensions, ScrollView, StyleSheet} from "react-native";
|
||||||
import React, { useState } from "react";
|
import React, {useState} from "react";
|
||||||
import { View, Text, Button } from "react-native-ui-lib";
|
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||||
import DumpList from "./DumpList";
|
import DumpList from "./DumpList";
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||||
import { TextField } from "react-native-ui-lib";
|
import {Feather, MaterialIcons} from "@expo/vector-icons";
|
||||||
import { StyleSheet } from "react-native";
|
|
||||||
import { Feather, MaterialIcons } from "@expo/vector-icons";
|
|
||||||
import { TextInput } from "react-native-gesture-handler";
|
|
||||||
import AddBrainDump from "./AddBrainDump";
|
import AddBrainDump from "./AddBrainDump";
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
import LinearGradient from "react-native-linear-gradient";
|
||||||
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
const BrainDumpPage = () => {
|
const BrainDumpPage = () => {
|
||||||
const [searchText, setSearchText] = useState<string>("");
|
const [searchText, setSearchText] = useState<string>("");
|
||||||
@ -27,7 +25,7 @@ const BrainDumpPage = () => {
|
|||||||
isWelcome={false}
|
isWelcome={false}
|
||||||
children={
|
children={
|
||||||
<Text
|
<Text
|
||||||
style={{ fontFamily: "Manrope_400Regular", fontSize: 14 }}
|
style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
|
||||||
>
|
>
|
||||||
Drop your notes on-the-go here, and{"\n"}organize them later.
|
Drop your notes on-the-go here, and{"\n"}organize them later.
|
||||||
</Text>
|
</Text>
|
||||||
@ -45,7 +43,7 @@ const BrainDumpPage = () => {
|
|||||||
name="search"
|
name="search"
|
||||||
size={24}
|
size={24}
|
||||||
color="#9b9b9b"
|
color="#9b9b9b"
|
||||||
style={{ paddingRight: 10 }}
|
style={{paddingRight: 10}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
@ -55,29 +53,28 @@ const BrainDumpPage = () => {
|
|||||||
placeholder="Search notes..."
|
placeholder="Search notes..."
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<DumpList searchText={searchText} />
|
<DumpList searchText={searchText}/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={["#f2f2f2", "transparent"]}
|
colors={["#f9f8f700", "#f9f8f7"]}
|
||||||
start={{ x: 0.5, y: 1 }}
|
locations={[0,1]}
|
||||||
end={{ x: 0.5, y: 0 }}
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: 90,
|
height: 120,
|
||||||
width: Dimensions.get("screen").width,
|
width: Dimensions.get("screen").width,
|
||||||
|
justifyContent:'center',
|
||||||
|
alignItems:"center"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
height: 40,
|
height: 40,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
marginLeft: "auto",
|
width: "90%",
|
||||||
width: 20,
|
|
||||||
right: 20,
|
|
||||||
bottom: -10,
|
bottom: -10,
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
backgroundColor: "#fd1775",
|
backgroundColor: "#fd1775",
|
||||||
@ -89,10 +86,10 @@ const BrainDumpPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View row centerV centerH>
|
<View row centerV centerH>
|
||||||
<MaterialIcons name="add" size={22} color={"white"} />
|
<PlusIcon />
|
||||||
<Text
|
<Text
|
||||||
white
|
white
|
||||||
style={{ fontSize: 16, fontFamily: "Manrope_600SemiBold" }}
|
style={{fontSize: 16, fontFamily: "Manrope_600SemiBold", marginLeft: 5}}
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
TouchableOpacity,
|
TouchableOpacity, TextFieldRef,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import { Dimensions, StyleSheet } from "react-native";
|
import { Dimensions, StyleSheet } from "react-native";
|
||||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||||
@ -18,6 +18,7 @@ import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
|||||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
import RemindersIcon from "@/assets/svgs/RemindersIcon";
|
import RemindersIcon from "@/assets/svgs/RemindersIcon";
|
||||||
import MenuIcon from "@/assets/svgs/MenuIcon";
|
import MenuIcon from "@/assets/svgs/MenuIcon";
|
||||||
|
import BrainDumpDialog from "./BrainDumpDialog";
|
||||||
|
|
||||||
const MoveBrainDump = (props: {
|
const MoveBrainDump = (props: {
|
||||||
item: IBrainDump;
|
item: IBrainDump;
|
||||||
@ -28,12 +29,35 @@ const MoveBrainDump = (props: {
|
|||||||
const [description, setDescription] = useState<string>(
|
const [description, setDescription] = useState<string>(
|
||||||
props.item.description
|
props.item.description
|
||||||
);
|
);
|
||||||
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
|
const descriptionRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
const { width } = Dimensions.get("screen");
|
const { width } = Dimensions.get("screen");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateBrainDumpItem(props.item.id, { description: description });
|
updateBrainDumpItem(props.item.id, { description: description });
|
||||||
}, [description]);
|
}, [description]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isVisible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
descriptionRef?.current?.focus()
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}, [props.isVisible]);
|
||||||
|
|
||||||
|
const showConfirmationDialog = () => {
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteNote = () =>{
|
||||||
|
deleteBrainDump(props.item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideConfirmationDialog = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
bottom={true}
|
bottom={true}
|
||||||
@ -81,8 +105,7 @@ const MoveBrainDump = (props: {
|
|||||||
marginL-5
|
marginL-5
|
||||||
iconSource={() => <BinIcon />}
|
iconSource={() => <BinIcon />}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
deleteBrainDump(props.item.id);
|
showConfirmationDialog();
|
||||||
props.setIsVisible(false);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -98,7 +121,6 @@ const MoveBrainDump = (props: {
|
|||||||
<TextField
|
<TextField
|
||||||
textAlignVertical="top"
|
textAlignVertical="top"
|
||||||
multiline
|
multiline
|
||||||
autoFocus
|
|
||||||
fieldStyle={{
|
fieldStyle={{
|
||||||
width: "94%",
|
width: "94%",
|
||||||
}}
|
}}
|
||||||
@ -109,6 +131,7 @@ const MoveBrainDump = (props: {
|
|||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
setDescription(value);
|
setDescription(value);
|
||||||
}}
|
}}
|
||||||
|
ref={descriptionRef}
|
||||||
returnKeyType="default"
|
returnKeyType="default"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -145,6 +168,7 @@ const MoveBrainDump = (props: {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
<BrainDumpDialog visible={modalVisible} title={props.item.title} onDismiss={hideConfirmationDialog} onConfirm={handleDeleteNote} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -191,10 +215,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontFamily: "Manrope_500Medium",
|
fontFamily: "Manrope_500Medium",
|
||||||
},
|
},
|
||||||
description:{
|
description: {
|
||||||
fontFamily: "Manrope_400Regular",
|
fontFamily: "Manrope_400Regular",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MoveBrainDump;
|
export default MoveBrainDump;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import CalendarIcon from "@/assets/svgs/CalendarIcon";
|
|||||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
import {useSetAtom} from "jotai";
|
import {useSetAtom} from "jotai";
|
||||||
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
import {selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
||||||
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
export const AddEventDialog = () => {
|
export const AddEventDialog = () => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
@ -50,8 +51,8 @@ export const AddEventDialog = () => {
|
|||||||
onPress={() => setShow(true)}
|
onPress={() => setShow(true)}
|
||||||
>
|
>
|
||||||
<View row centerV centerH>
|
<View row centerV centerH>
|
||||||
<MaterialIcons name="add" size={22} color={"white"}/>
|
<PlusIcon />
|
||||||
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold'}}>
|
<Text white style={{fontSize: 16, fontFamily: 'Manrope_600SemiBold', marginLeft: 5}}>
|
||||||
New
|
New
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
import React, {memo} from 'react';
|
import React, { memo } from "react";
|
||||||
import {Button, Picker, PickerModes, SegmentedControl, Text, View} from "react-native-ui-lib";
|
import {
|
||||||
import {MaterialIcons} from "@expo/vector-icons";
|
Button,
|
||||||
import {modeMap, months} from './constants';
|
Picker,
|
||||||
import {StyleSheet} from "react-native";
|
PickerModes,
|
||||||
import {useAtom} from "jotai";
|
SegmentedControl,
|
||||||
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
|
Text,
|
||||||
import {isSameDay} from "date-fns";
|
View,
|
||||||
|
} from "react-native-ui-lib";
|
||||||
|
import { MaterialIcons } from "@expo/vector-icons";
|
||||||
|
import { modeMap, months } from "./constants";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { modeAtom, selectedDateAtom } from "@/components/pages/calendar/atoms";
|
||||||
|
import { format, isSameDay } from "date-fns";
|
||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
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);
|
||||||
@ -29,7 +38,7 @@ export const CalendarHeader = memo(() => {
|
|||||||
setSelectedDate(updatedDate);
|
setSelectedDate(updatedDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelectedDateToday = isSameDay(selectedDate, new Date())
|
const isSelectedDateToday = isSameDay(selectedDate, new Date());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -47,38 +56,56 @@ 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}}
|
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>
|
||||||
|
|
||||||
<View row>
|
<View row centerV>
|
||||||
{!isSelectedDateToday && (
|
{!isSelectedDateToday && (
|
||||||
<Button size={"small"} marginR-5 label={"Today"} onPress={() => {
|
<Button
|
||||||
setSelectedDate(new Date())
|
size={"xSmall"}
|
||||||
setMode("day")
|
marginR-0
|
||||||
}}/>
|
avoidInnerPadding
|
||||||
|
style={{
|
||||||
|
borderRadius: 50,
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderWidth: 0.7,
|
||||||
|
borderColor: "#dadce0",
|
||||||
|
height: 30,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
}}
|
||||||
|
labelStyle={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: "black",
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
}}
|
||||||
|
label={format(new Date(), "dd/MM/yyyy")}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedDate(new Date());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View>
|
<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"
|
||||||
@ -92,8 +119,7 @@ export const CalendarHeader = memo(() => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default function CalendarPage() {
|
|||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Let's get your week started!"}
|
message={"Let's get your week started!"}
|
||||||
isWelcome
|
isWelcome
|
||||||
|
isCalendar={true}
|
||||||
/>
|
/>
|
||||||
<InnerCalendar/>
|
<InnerCalendar/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React from "react";
|
||||||
import {StyleSheet} from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import {useSetAtom} from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import {isFamilyViewAtom} from "@/components/pages/calendar/atoms";
|
import { isFamilyViewAtom } from "@/components/pages/calendar/atoms";
|
||||||
|
|
||||||
|
|
||||||
const CalendarViewSwitch = () => {
|
const CalendarViewSwitch = () => {
|
||||||
const [calView, setCalView] = useState<boolean>(false);
|
const [isFamilyView, setIsFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
const viewSwitch = useSetAtom(isFamilyViewAtom)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -23,7 +21,7 @@ const CalendarViewSwitch = () => {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
// iOS shadow
|
// iOS shadow
|
||||||
shadowColor: "#000",
|
shadowColor: "#000",
|
||||||
shadowOffset: {width: 0, height: 2},
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowOpacity: 0.25,
|
shadowOpacity: 0.25,
|
||||||
shadowRadius: 3.84,
|
shadowRadius: 3.84,
|
||||||
// Android shadow (elevation)
|
// Android shadow (elevation)
|
||||||
@ -33,8 +31,7 @@ const CalendarViewSwitch = () => {
|
|||||||
>
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setCalView(true);
|
setIsFamilyView(true);
|
||||||
viewSwitch(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -42,9 +39,12 @@ const CalendarViewSwitch = () => {
|
|||||||
centerH
|
centerH
|
||||||
height={40}
|
height={40}
|
||||||
paddingH-15
|
paddingH-15
|
||||||
style={calView ? styles.switchBtnActive : styles.switchBtn}
|
style={isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color={isFamilyView ? "white" : "#a1a1a1"}
|
||||||
|
style={styles.switchTxt}
|
||||||
>
|
>
|
||||||
<Text color={calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
|
|
||||||
Family View
|
Family View
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -52,8 +52,7 @@ const CalendarViewSwitch = () => {
|
|||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setCalView(false);
|
setIsFamilyView(false);
|
||||||
viewSwitch(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -61,9 +60,12 @@ const CalendarViewSwitch = () => {
|
|||||||
centerH
|
centerH
|
||||||
height={40}
|
height={40}
|
||||||
paddingH-15
|
paddingH-15
|
||||||
style={!calView ? styles.switchBtnActive : styles.switchBtn}
|
style={!isFamilyView ? styles.switchBtnActive : styles.switchBtn}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color={!isFamilyView ? "white" : "#a1a1a1"}
|
||||||
|
style={styles.switchTxt}
|
||||||
>
|
>
|
||||||
<Text color={!calView ? "white" : "#a1a1a1"} style={styles.switchTxt}>
|
|
||||||
My View
|
My View
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -85,6 +87,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
switchTxt: {
|
switchTxt: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: 'Manrope_600SemiBold'
|
fontFamily: "Manrope_600SemiBold",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
81
components/pages/calendar/DeleteEventDialog.tsx
Normal file
81
components/pages/calendar/DeleteEventDialog.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
interface DeleteEventDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteEventDialog: React.FC<DeleteEventDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
onDismiss,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<Text center style={styles.title}>
|
||||||
|
Delete Event
|
||||||
|
</Text>
|
||||||
|
<View center>
|
||||||
|
<Text style={styles.text} center>
|
||||||
|
Are you sure you want to delete the event:{" "}
|
||||||
|
<Text style={{ fontSize: 16, fontFamily: "PlusJakartaSans_700Bold" }}>
|
||||||
|
{title}
|
||||||
|
</Text>{" "}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View row right gap-8>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={onDismiss}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={onConfirm}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stylesheet for future styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
confirmBtn: {
|
||||||
|
backgroundColor: "#ea156d",
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
paddingTop: 35,
|
||||||
|
paddingBottom: 17,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DeleteEventDialog;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
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, StyleSheet, View} 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, useSetAtom} from "jotai";
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
import {
|
import {
|
||||||
@ -8,10 +8,12 @@ import {
|
|||||||
eventForEditAtom,
|
eventForEditAtom,
|
||||||
modeAtom,
|
modeAtom,
|
||||||
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 {addDays, compareAsc, isWithinInterval, subDays} from "date-fns";
|
||||||
|
|
||||||
interface EventCalendarProps {
|
interface EventCalendarProps {
|
||||||
calendarHeight: number;
|
calendarHeight: number;
|
||||||
@ -22,9 +24,10 @@ interface EventCalendarProps {
|
|||||||
const getTotalMinutes = () => {
|
const getTotalMinutes = () => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
return Math.abs(date.getUTCHours() * 60 + date.getUTCMinutes() - 200);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(({calendarHeight}) => {
|
export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
|
||||||
|
({calendarHeight}) => {
|
||||||
const {data: events, isLoading} = useGetEvents();
|
const {data: events, isLoading} = useGetEvents();
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
const [selectedDate, setSelectedDate] = useAtom(selectedDateAtom);
|
||||||
@ -34,45 +37,42 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(({calendar
|
|||||||
const setEventForEdit = useSetAtom(eventForEditAtom);
|
const setEventForEdit = useSetAtom(eventForEditAtom);
|
||||||
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
const setSelectedNewEndDate = useSetAtom(selectedNewEventDateAtom);
|
||||||
|
|
||||||
const [isRendering, setIsRendering] = useState(true);
|
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes());
|
||||||
const [offsetMinutes, setOffsetMinutes] = useState(getTotalMinutes())
|
|
||||||
|
|
||||||
useEffect(() => {
|
const todaysDate = new Date();
|
||||||
if (events && mode) {
|
|
||||||
setIsRendering(true);
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
setIsRendering(false);
|
|
||||||
}, 300);
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}, [events, mode]);
|
|
||||||
|
|
||||||
const handlePressEvent = useCallback((event: CalendarEvent) => {
|
const handlePressEvent = useCallback(
|
||||||
|
(event: CalendarEvent) => {
|
||||||
if (mode === "day" || mode === "week") {
|
if (mode === "day" || mode === "week") {
|
||||||
setEditVisible(true);
|
setEditVisible(true);
|
||||||
console.log({event})
|
console.log({event});
|
||||||
setEventForEdit(event);
|
setEventForEdit(event);
|
||||||
} else {
|
} else {
|
||||||
setMode("day")
|
setMode("day");
|
||||||
setSelectedDate(event.start);
|
setSelectedDate(event.start);
|
||||||
}
|
}
|
||||||
}, [setEditVisible, setEventForEdit, mode]);
|
},
|
||||||
|
[setEditVisible, setEventForEdit, mode]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePressCell = useCallback(
|
const handlePressCell = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
if (mode === "day" || mode === "week") {
|
if (mode === "day" || mode === "week") {
|
||||||
setSelectedNewEndDate(date);
|
setSelectedNewEndDate(date);
|
||||||
} else {
|
} else {
|
||||||
setMode("day")
|
setMode("day");
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[mode, setSelectedNewEndDate, setSelectedDate]
|
[mode, setSelectedNewEndDate, setSelectedDate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSwipeEnd = useCallback((date: Date) => {
|
const handleSwipeEnd = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
}, [setSelectedDate]);
|
},
|
||||||
|
[setSelectedDate]
|
||||||
|
);
|
||||||
|
|
||||||
const memoizedEventCellStyle = useCallback(
|
const memoizedEventCellStyle = useCallback(
|
||||||
(event: CalendarEvent) => ({backgroundColor: event.eventColor}),
|
(event: CalendarEvent) => ({backgroundColor: event.eventColor}),
|
||||||
@ -84,18 +84,131 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(({calendar
|
|||||||
[profileData]
|
[profileData]
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoizedHeaderContentStyle = useMemo(
|
console.log({memoizedWeekStartsOn, profileData: profileData?.firstDayOfWeek})
|
||||||
() => (mode === "day" ? styles.dayModeHeader : {}),
|
|
||||||
[mode]
|
const isSameDate = useCallback((date1: Date, date2: Date) => {
|
||||||
|
return (
|
||||||
|
date1.getDate() === date2.getDate() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getFullYear() === date2.getFullYear()
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dayHeaderColor = useMemo(() => {
|
||||||
|
return isSameDate(todaysDate, selectedDate) ? "white" : "#4d4d4d";
|
||||||
|
}, [selectedDate, mode]);
|
||||||
|
|
||||||
|
const dateStyle = useMemo(() => {
|
||||||
|
if (mode === "week") return undefined;
|
||||||
|
return isSameDate(todaysDate, selectedDate) && mode === "day"
|
||||||
|
? styles.dayHeader
|
||||||
|
: styles.otherDayHeader;
|
||||||
|
}, [selectedDate, mode]);
|
||||||
|
|
||||||
|
const memoizedHeaderContentStyle = useMemo(() => {
|
||||||
|
if (mode === "day") {
|
||||||
|
return styles.dayModeHeader;
|
||||||
|
} else if (mode === "week") {
|
||||||
|
return styles.weekModeHeader;
|
||||||
|
} else if (mode === "month") {
|
||||||
|
return styles.monthModeHeader;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
|
||||||
|
const {enrichedEvents, filteredEvents} = useMemo(() => {
|
||||||
|
const startTime = Date.now(); // Start timer
|
||||||
|
|
||||||
|
const startOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||||
|
const endOffset = mode === "month" ? 40 : mode === "week" ? 10 : 1;
|
||||||
|
|
||||||
|
const filteredEvents = events?.filter(event =>
|
||||||
|
event.start && event.end &&
|
||||||
|
isWithinInterval(event.start, {
|
||||||
|
start: subDays(selectedDate, startOffset),
|
||||||
|
end: addDays(selectedDate, endOffset)
|
||||||
|
}) &&
|
||||||
|
isWithinInterval(event.end, {
|
||||||
|
start: subDays(selectedDate, startOffset),
|
||||||
|
end: addDays(selectedDate, endOffset)
|
||||||
|
})
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const enrichedEvents = filteredEvents.reduce((acc, event) => {
|
||||||
|
const dateKey = event.start.toISOString().split('T')[0];
|
||||||
|
acc[dateKey] = acc[dateKey] || [];
|
||||||
|
acc[dateKey].push({
|
||||||
|
...event,
|
||||||
|
overlapPosition: false,
|
||||||
|
overlapCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
acc[dateKey].sort((a, b) => compareAsc(a.start, b.start));
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, CalendarEvent[]>);
|
||||||
|
|
||||||
|
const endTime = Date.now();
|
||||||
|
console.log("memoizedEvents computation time:", endTime - startTime, "ms");
|
||||||
|
|
||||||
|
return {enrichedEvents, filteredEvents};
|
||||||
|
}, [events, selectedDate, mode]);
|
||||||
|
|
||||||
|
const renderCustomDateForMonth = (date: Date) => {
|
||||||
|
const circleStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 15,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoizedEvents = useMemo(() => events ?? [], [events]);
|
const defaultStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
...circleStyle,
|
||||||
|
}),
|
||||||
|
[circleStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentDateStyle = useMemo<ViewStyle>(
|
||||||
|
() => ({
|
||||||
|
...circleStyle,
|
||||||
|
backgroundColor: "#4184f2",
|
||||||
|
}),
|
||||||
|
[circleStyle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDate = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
const isCurrentDate = isSameDate(todaysDate, date);
|
||||||
|
const appliedStyle = isCurrentDate ? currentDateStyle : defaultStyle;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{alignItems: "center"}}>
|
||||||
|
<View style={appliedStyle}>
|
||||||
|
<Text style={{color: isCurrentDate ? "white" : "black"}}>
|
||||||
|
{date.getDate()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[todaysDate, currentDateStyle, defaultStyle] // dependencies
|
||||||
|
);
|
||||||
|
|
||||||
|
return renderDate(date);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOffsetMinutes(getTotalMinutes())
|
setOffsetMinutes(getTotalMinutes());
|
||||||
}, [events, mode]);
|
}, [events, mode]);
|
||||||
|
|
||||||
if (isLoading || isRendering) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color="#0000ff"/>
|
<ActivityIndicator size="large" color="#0000ff"/>
|
||||||
@ -103,26 +216,58 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(({calendar
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(enrichedEvents, filteredEvents)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
bodyContainerStyle={styles.calHeader}
|
bodyContainerStyle={styles.calHeader}
|
||||||
swipeEnabled
|
swipeEnabled
|
||||||
enableEnrichedEvents
|
|
||||||
mode={mode}
|
mode={mode}
|
||||||
events={memoizedEvents}
|
enableEnrichedEvents={true}
|
||||||
eventCellStyle={memoizedEventCellStyle}
|
sortedMonthView
|
||||||
|
// enrichedEventsByDate={enrichedEvents}
|
||||||
|
events={filteredEvents}
|
||||||
|
// eventCellStyle={memoizedEventCellStyle}
|
||||||
onPressEvent={handlePressEvent}
|
onPressEvent={handlePressEvent}
|
||||||
weekStartsOn={memoizedWeekStartsOn}
|
weekStartsOn={memoizedWeekStartsOn}
|
||||||
height={calendarHeight}
|
height={calendarHeight}
|
||||||
activeDate={selectedDate}
|
activeDate={todaysDate}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
onPressCell={handlePressCell}
|
onPressCell={handlePressCell}
|
||||||
headerContentStyle={memoizedHeaderContentStyle}
|
headerContentStyle={memoizedHeaderContentStyle}
|
||||||
onSwipeEnd={handleSwipeEnd}
|
onSwipeEnd={handleSwipeEnd}
|
||||||
scrollOffsetMinutes={offsetMinutes}
|
scrollOffsetMinutes={offsetMinutes}
|
||||||
|
theme={{
|
||||||
|
palette: {
|
||||||
|
nowIndicator: profileData?.eventColor || "#fd1575",
|
||||||
|
gray: {
|
||||||
|
"100": "#e8eaed",
|
||||||
|
"200": "#e8eaed",
|
||||||
|
"500": "#b7b7b7",
|
||||||
|
"800": "#919191",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
sm: {fontFamily: "Manrope_600SemiBold", fontSize: 15},
|
||||||
|
xl: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
moreLabel: {},
|
||||||
|
xs: {fontSize: 10},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dayHeaderStyle={dateStyle}
|
||||||
|
dayHeaderHighlightColor={"white"}
|
||||||
|
showAdjacentMonths
|
||||||
|
hourStyle={styles.hourStyle}
|
||||||
|
ampm
|
||||||
|
// renderCustomDateForMonth={renderCustomDateForMonth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
segmentslblStyle: {
|
segmentslblStyle: {
|
||||||
@ -138,10 +283,33 @@ const styles = StyleSheet.create({
|
|||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
width: 38,
|
width: 38,
|
||||||
right: 42,
|
right: 42,
|
||||||
|
height: 13,
|
||||||
},
|
},
|
||||||
|
weekModeHeader: {},
|
||||||
|
monthModeHeader: {},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
dayHeader: {
|
||||||
|
backgroundColor: "#4184f2",
|
||||||
|
aspectRatio: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
otherDayHeader: {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
color: "#919191",
|
||||||
|
aspectRatio: 1,
|
||||||
|
borderRadius: 100,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
hourStyle: {
|
||||||
|
color: "#5f6368",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -17,7 +17,7 @@ import {
|
|||||||
import {ScrollView} from "react-native-gesture-handler";
|
import {ScrollView} from "react-native-gesture-handler";
|
||||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
import {useEffect, useRef, useState} from "react";
|
import {useEffect, useRef, useState} from "react";
|
||||||
import {AntDesign, Feather, Ionicons,} from "@expo/vector-icons";
|
import {AntDesign, Feather, Ionicons} from "@expo/vector-icons";
|
||||||
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
|
import {PickerMultiValue} from "react-native-ui-lib/src/components/picker/types";
|
||||||
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEvent} from "@/hooks/firebase/useCreateEvent";
|
||||||
import {EventData} from "@/hooks/firebase/types/eventData";
|
import {EventData} from "@/hooks/firebase/types/eventData";
|
||||||
@ -30,8 +30,11 @@ import MenuIcon from "@/assets/svgs/MenuIcon";
|
|||||||
import CameraIcon from "@/assets/svgs/CameraIcon";
|
import CameraIcon from "@/assets/svgs/CameraIcon";
|
||||||
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
|
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
|
||||||
import {useAtom} from "jotai";
|
import {useAtom} from "jotai";
|
||||||
import {eventForEditAtom, selectedNewEventDateAtom} from "@/components/pages/calendar/atoms";
|
import {eventForEditAtom, selectedNewEventDateAtom,} from "@/components/pages/calendar/atoms";
|
||||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
import BinIcon from "@/assets/svgs/BinIcon";
|
||||||
|
import DeleteEventDialog from "./DeleteEventDialog";
|
||||||
|
import {useDeleteEvent} from "@/hooks/firebase/useDeleteEvent";
|
||||||
|
|
||||||
const daysOfWeek = [
|
const daysOfWeek = [
|
||||||
{label: "Monday", value: "monday"},
|
{label: "Monday", value: "monday"},
|
||||||
@ -46,24 +49,31 @@ const daysOfWeek = [
|
|||||||
export const ManuallyAddEventModal = () => {
|
export const ManuallyAddEventModal = () => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(selectedNewEventDateAtom)
|
const [selectedNewEventDate, setSelectedNewEndDate] = useAtom(
|
||||||
const [editEvent, setEditEvent] = useAtom(eventForEditAtom)
|
selectedNewEventDateAtom
|
||||||
|
);
|
||||||
|
const [editEvent, setEditEvent] = useAtom(eventForEditAtom);
|
||||||
|
const [deleteModalVisible, setDeleteModalVisible] = useState<boolean>(false);
|
||||||
|
const {mutateAsync: deleteEvent, isLoading: isDeleting} = useDeleteEvent()
|
||||||
|
|
||||||
const {show, close, initialDate} = {
|
const {show, close, initialDate} = {
|
||||||
show: !!selectedNewEventDate || !!editEvent,
|
show: !!selectedNewEventDate || !!editEvent,
|
||||||
close: () => {
|
close: () => {
|
||||||
setSelectedNewEndDate(undefined)
|
setDeleteModalVisible(false);
|
||||||
setEditEvent(undefined)
|
setSelectedNewEndDate(undefined);
|
||||||
|
setEditEvent(undefined);
|
||||||
},
|
},
|
||||||
initialDate: selectedNewEventDate || editEvent?.start
|
initialDate: selectedNewEventDate || editEvent?.start,
|
||||||
}
|
};
|
||||||
|
|
||||||
const detailsRef = useRef<TextFieldRef>(null)
|
const detailsRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>(editEvent?.title || "");
|
const [title, setTitle] = useState<string>(editEvent?.title || "");
|
||||||
const [details, setDetails] = useState<string>(editEvent?.notes || "");
|
const [details, setDetails] = useState<string>(editEvent?.notes || "");
|
||||||
const [isAllDay, setIsAllDay] = useState(editEvent?.allDay || false);
|
const [isAllDay, setIsAllDay] = useState(editEvent?.allDay || false);
|
||||||
const [isPrivate, setIsPrivate] = useState<boolean>(editEvent?.private || false);
|
const [isPrivate, setIsPrivate] = useState<boolean>(
|
||||||
|
editEvent?.private || false
|
||||||
|
);
|
||||||
const [startTime, setStartTime] = useState(() => {
|
const [startTime, setStartTime] = useState(() => {
|
||||||
const date = initialDate ?? new Date();
|
const date = initialDate ?? new Date();
|
||||||
date.setSeconds(0, 0);
|
date.setSeconds(0, 0);
|
||||||
@ -75,19 +85,27 @@ export const ManuallyAddEventModal = () => {
|
|||||||
date.setSeconds(0, 0);
|
date.setSeconds(0, 0);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
const date = (editEvent?.end ?? initialDate)
|
const date =
|
||||||
? addHours((editEvent?.end ?? initialDate!), 1)
|
editEvent?.end ?? initialDate
|
||||||
|
? addHours(editEvent?.end ?? initialDate!, 1)
|
||||||
: addHours(new Date(), 1);
|
: addHours(new Date(), 1);
|
||||||
date.setSeconds(0, 0);
|
date.setSeconds(0, 0);
|
||||||
return date;
|
return date;
|
||||||
});
|
});
|
||||||
const [startDate, setStartDate] = useState(initialDate ?? new Date());
|
const [startDate, setStartDate] = useState(initialDate ?? new Date());
|
||||||
const [endDate, setEndDate] = useState(editEvent?.end ?? initialDate ?? new Date());
|
const [endDate, setEndDate] = useState(
|
||||||
const [selectedAttendees, setSelectedAttendees] = useState<string[]>(editEvent?.participants ?? []);
|
editEvent?.end ?? initialDate ?? new Date()
|
||||||
|
);
|
||||||
|
const [selectedAttendees, setSelectedAttendees] = useState<string[]>(
|
||||||
|
editEvent?.participants ?? []
|
||||||
|
);
|
||||||
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
|
const [repeatInterval, setRepeatInterval] = useState<PickerMultiValue>([]);
|
||||||
|
|
||||||
const {mutateAsync: createEvent, isLoading, isError} = useCreateEvent();
|
const {mutateAsync: createEvent, isLoading: isAdding, isError} = useCreateEvent();
|
||||||
const {data: members} = useGetFamilyMembers(true);
|
const {data: members} = useGetFamilyMembers(true);
|
||||||
|
const titleRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
|
const isLoading = isDeleting || isAdding
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(editEvent?.title || "");
|
setTitle(editEvent?.title || "");
|
||||||
@ -105,8 +123,9 @@ export const ManuallyAddEventModal = () => {
|
|||||||
date.setSeconds(0, 0);
|
date.setSeconds(0, 0);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
const date = (editEvent?.end ?? initialDate)
|
const date =
|
||||||
? addHours((editEvent?.end ?? initialDate!), 1)
|
editEvent?.end ?? initialDate
|
||||||
|
? addHours(editEvent?.end ?? initialDate!, 1)
|
||||||
: addHours(new Date(), 1);
|
: addHours(new Date(), 1);
|
||||||
date.setSeconds(0, 0);
|
date.setSeconds(0, 0);
|
||||||
return date;
|
return date;
|
||||||
@ -117,6 +136,14 @@ export const ManuallyAddEventModal = () => {
|
|||||||
setRepeatInterval([]);
|
setRepeatInterval([]);
|
||||||
}, [editEvent, selectedNewEventDate]);
|
}, [editEvent, selectedNewEventDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(show && !editEvent) {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus()
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [selectedNewEventDate]);
|
||||||
|
|
||||||
if (!show) return null;
|
if (!show) return null;
|
||||||
|
|
||||||
const formatDateTime = (date?: Date | string) => {
|
const formatDateTime = (date?: Date | string) => {
|
||||||
@ -128,6 +155,19 @@ export const ManuallyAddEventModal = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showDeleteEventModal = () => {
|
||||||
|
setDeleteModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEvent = async () => {
|
||||||
|
await deleteEvent({eventId: `${editEvent?.id}`})
|
||||||
|
close()
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideDeleteEventModal = () => {
|
||||||
|
setDeleteModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
const combineDateAndTime = (date: Date, time: Date): Date => {
|
const combineDateAndTime = (date: Date, time: Date): Date => {
|
||||||
const combined = new Date(date);
|
const combined = new Date(date);
|
||||||
combined.setHours(time.getHours());
|
combined.setHours(time.getHours());
|
||||||
@ -155,12 +195,13 @@ export const ManuallyAddEventModal = () => {
|
|||||||
endDate: finalEndDate,
|
endDate: finalEndDate,
|
||||||
allDay: isAllDay,
|
allDay: isAllDay,
|
||||||
attendees: selectedAttendees,
|
attendees: selectedAttendees,
|
||||||
notes: details
|
notes: details,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editEvent?.id) eventData.id = editEvent?.id
|
if (editEvent?.id) eventData.id = editEvent?.id;
|
||||||
|
|
||||||
await createEvent(eventData);
|
await createEvent(eventData);
|
||||||
|
setEditEvent(undefined);
|
||||||
|
|
||||||
close();
|
close();
|
||||||
};
|
};
|
||||||
@ -205,7 +246,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
onRequestClose={close}
|
onRequestClose={close}
|
||||||
transparent={false}
|
transparent={false}
|
||||||
>
|
>
|
||||||
<LoaderScreen message={"Saving event..."} color={Colors.grey40}/>
|
<LoaderScreen message={isDeleting ? "Deleting event..." : "Saving event..."} color={Colors.grey40}/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -227,6 +268,39 @@ export const ManuallyAddEventModal = () => {
|
|||||||
paddingRight: insets.right, // Safe area inset for right
|
paddingRight: insets.right, // Safe area inset for right
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/*{editEvent ? (*/}
|
||||||
|
{/* <>*/}
|
||||||
|
{/* <View center paddingT-8>*/}
|
||||||
|
{/* <TouchableOpacity onPress={close}>*/}
|
||||||
|
{/* <DropModalIcon />*/}
|
||||||
|
{/* </TouchableOpacity>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* <View row spread paddingH-10 paddingB-15>*/}
|
||||||
|
{/* <Button*/}
|
||||||
|
{/* color="#05a8b6"*/}
|
||||||
|
{/* style={styles.topBtn}*/}
|
||||||
|
{/* iconSource={() => <CloseXIcon />}*/}
|
||||||
|
{/* onPress={close}*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* <View row>*/}
|
||||||
|
{/* <Button*/}
|
||||||
|
{/* style={styles.topBtn}*/}
|
||||||
|
{/* marginR-10*/}
|
||||||
|
{/* iconSource={() => <PenIcon />}*/}
|
||||||
|
{/* onPress={handleSave}*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* <Button*/}
|
||||||
|
{/* style={styles.topBtn}*/}
|
||||||
|
{/* marginL-5*/}
|
||||||
|
{/* iconSource={() => <BinIcon />}*/}
|
||||||
|
{/* onPress={() => {*/}
|
||||||
|
{/* showDeleteEventModal();*/}
|
||||||
|
{/* }}*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </View>*/}
|
||||||
|
{/* </>*/}
|
||||||
|
{/*) : (*/}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@ -246,7 +320,10 @@ export const ManuallyAddEventModal = () => {
|
|||||||
Cancel
|
Cancel
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<View row center>
|
||||||
<DropModalIcon onPress={close}/>
|
<DropModalIcon onPress={close}/>
|
||||||
|
</View>
|
||||||
|
<View flexS row gap-10>
|
||||||
<TouchableOpacity onPress={handleSave}>
|
<TouchableOpacity onPress={handleSave}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@ -259,12 +336,23 @@ export const ManuallyAddEventModal = () => {
|
|||||||
Save
|
Save
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
{editEvent && (
|
||||||
|
<Button
|
||||||
|
style={styles.topBtn}
|
||||||
|
marginL-5
|
||||||
|
iconSource={() => <BinIcon/>}
|
||||||
|
onPress={showDeleteEventModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
{/*)}*/}
|
||||||
<ScrollView style={{minHeight: "85%"}}>
|
<ScrollView style={{minHeight: "85%"}}>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Add event title"
|
placeholder="Add event title"
|
||||||
|
ref={titleRef}
|
||||||
value={title}
|
value={title}
|
||||||
autoFocus
|
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setTitle(text);
|
setTitle(text);
|
||||||
}}
|
}}
|
||||||
@ -307,7 +395,7 @@ export const ManuallyAddEventModal = () => {
|
|||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
setStartDate(date);
|
setStartDate(date);
|
||||||
}}
|
}}
|
||||||
maximumDate={endDate}
|
//maximumDate={endDate}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@ -317,8 +405,11 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={startTime}
|
value={startTime}
|
||||||
onChange={(date) => setStartTime(date)}
|
onChange={(time) => {
|
||||||
maximumDate={endTime}
|
if (time <= endTime) {
|
||||||
|
setStartTime(time);
|
||||||
|
}
|
||||||
|
}}
|
||||||
minuteInterval={5}
|
minuteInterval={5}
|
||||||
dateTimeFormatter={(date, mode) =>
|
dateTimeFormatter={(date, mode) =>
|
||||||
date.toLocaleTimeString("en-us", {
|
date.toLocaleTimeString("en-us", {
|
||||||
@ -355,7 +446,11 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={endTime}
|
value={endTime}
|
||||||
onChange={(date) => setEndTime(date)}
|
onChange={(time) => {
|
||||||
|
if (time >= endTime) {
|
||||||
|
setEndTime(time);
|
||||||
|
}
|
||||||
|
}}
|
||||||
minimumDate={startTime}
|
minimumDate={startTime}
|
||||||
minuteInterval={5}
|
minuteInterval={5}
|
||||||
dateTimeFormatter={(date, mode) =>
|
dateTimeFormatter={(date, mode) =>
|
||||||
@ -388,10 +483,12 @@ export const ManuallyAddEventModal = () => {
|
|||||||
<View flex-1/>
|
<View flex-1/>
|
||||||
<Picker
|
<Picker
|
||||||
value={selectedAttendees}
|
value={selectedAttendees}
|
||||||
onChange={(value) => setSelectedAttendees(value as string[] ?? [])}
|
onChange={(value) =>
|
||||||
|
setSelectedAttendees((value as string[]) ?? [])
|
||||||
|
}
|
||||||
style={{marginLeft: "auto"}}
|
style={{marginLeft: "auto"}}
|
||||||
mode={PickerModes.MULTI}
|
mode={PickerModes.MULTI}
|
||||||
renderInput={() =>
|
renderInput={() => (
|
||||||
<Button
|
<Button
|
||||||
size={ButtonSize.small}
|
size={ButtonSize.small}
|
||||||
paddingH-8
|
paddingH-8
|
||||||
@ -407,9 +504,13 @@ export const ManuallyAddEventModal = () => {
|
|||||||
}}
|
}}
|
||||||
color="#ea156c"
|
color="#ea156c"
|
||||||
label="Add"
|
label="Add"
|
||||||
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
|
labelStyle={{
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}>
|
)}
|
||||||
|
>
|
||||||
{members?.map((member) => (
|
{members?.map((member) => (
|
||||||
<Picker.Item
|
<Picker.Item
|
||||||
key={member?.uid}
|
key={member?.uid}
|
||||||
@ -421,8 +522,10 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View marginL-35>
|
<View marginL-35>
|
||||||
<AssigneesDisplay setSelectedAttendees={setSelectedAttendees}
|
<AssigneesDisplay
|
||||||
selectedAttendees={selectedAttendees}/>
|
setSelectedAttendees={setSelectedAttendees}
|
||||||
|
selectedAttendees={selectedAttendees}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.divider}/>
|
<View style={styles.divider}/>
|
||||||
@ -461,14 +564,15 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.divider}/>
|
<View style={styles.divider}/>
|
||||||
<View marginH-30 marginB-0 row spread centerV>
|
<View marginH-30 marginB-0 row spread centerV>
|
||||||
<View row centerH>
|
<View row center>
|
||||||
<LockIcon/>
|
<LockIcon/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
}}
|
}}
|
||||||
marginL-10
|
marginL-12
|
||||||
|
center
|
||||||
>
|
>
|
||||||
Mark as Private
|
Mark as Private
|
||||||
</Text>
|
</Text>
|
||||||
@ -500,14 +604,22 @@ export const ManuallyAddEventModal = () => {
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TextField value={details} onChangeText={setDetails} ref={detailsRef} maxLength={2000} multiline
|
<TextField
|
||||||
numberOfLines={10} marginT-10 style={{flex: 1, minHeight: 180}}/>
|
value={details}
|
||||||
|
onChangeText={setDetails}
|
||||||
|
ref={detailsRef}
|
||||||
|
maxLength={2000}
|
||||||
|
multiline
|
||||||
|
numberOfLines={10}
|
||||||
|
marginT-10
|
||||||
|
style={{flex: 1, minHeight: 180}}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<Button
|
<Button
|
||||||
disabled
|
disabled
|
||||||
marginH-30
|
marginH-30
|
||||||
marginB-15
|
marginB-30
|
||||||
label="Create event from image"
|
label="Create event from image"
|
||||||
text70
|
text70
|
||||||
style={{height: 47}}
|
style={{height: 47}}
|
||||||
@ -520,6 +632,14 @@ export const ManuallyAddEventModal = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
{editEvent && (
|
||||||
|
<DeleteEventDialog
|
||||||
|
visible={deleteModalVisible}
|
||||||
|
title={editEvent?.title}
|
||||||
|
onDismiss={hideDeleteEventModal}
|
||||||
|
onConfirm={handleDeleteEvent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from "jotai";
|
||||||
import {CalendarEvent} from "@/components/pages/calendar/interfaces";
|
import { CalendarEvent } from "@/components/pages/calendar/interfaces";
|
||||||
|
|
||||||
export const editVisibleAtom = atom<boolean>(false);
|
export const editVisibleAtom = atom<boolean>(false);
|
||||||
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
|
export const eventForEditAtom = atom<CalendarEvent | undefined>(undefined);
|
||||||
@ -7,3 +7,6 @@ export const isFamilyViewAtom = atom<boolean>(false);
|
|||||||
export const modeAtom = atom<"week" | "month" | "day">("week");
|
export const modeAtom = atom<"week" | "month" | "day">("week");
|
||||||
export const selectedDateAtom = atom<Date>(new Date());
|
export const selectedDateAtom = atom<Date>(new Date());
|
||||||
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);
|
export const selectedNewEventDateAtom = atom<Date | undefined>(undefined);
|
||||||
|
export const settingsPageIndex = atom<number>(0);
|
||||||
|
export const userSettingsView = atom<boolean>(true);
|
||||||
|
export const toDosPageIndex = atom<number>(0);
|
||||||
|
|||||||
@ -10,5 +10,7 @@ export interface CalendarEvent {
|
|||||||
eventColor?: string; // Optional color to represent the event
|
eventColor?: string; // Optional color to represent the event
|
||||||
participants?: string[]; // Optional list of participants or attendees
|
participants?: string[]; // Optional list of participants or attendees
|
||||||
private?: boolean;
|
private?: boolean;
|
||||||
notes?: string
|
notes?: string,
|
||||||
|
overlapPosition?: number
|
||||||
|
overlapCount?: number
|
||||||
}
|
}
|
||||||
157
components/pages/feedback/AddFeedback.tsx
Normal file
157
components/pages/feedback/AddFeedback.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
TextField,
|
||||||
|
TextFieldRef,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native-ui-lib";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||||
|
import { Dimensions, Platform, StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
|
import { useBrainDumpContext } from "@/contexts/DumpContext";
|
||||||
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import { useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
interface IAddFeedbackProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
setIsVisible: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddFeedback = ({
|
||||||
|
addFeedbackProps,
|
||||||
|
}: {
|
||||||
|
addFeedbackProps: IAddFeedbackProps;
|
||||||
|
}) => {
|
||||||
|
const { addFeedback } = useFeedbackContext();
|
||||||
|
const [feedback, setFeedback] = useState<string>("");
|
||||||
|
const [feedbackTitle, setFeedbackTitle] = useState<string>("");
|
||||||
|
const { width } = Dimensions.get("screen");
|
||||||
|
|
||||||
|
const descriptionRef = useRef<TextFieldRef>(null);
|
||||||
|
const titleRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFeedback("");
|
||||||
|
}, [addFeedbackProps.isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addFeedbackProps.isVisible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [addFeedbackProps.isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(false);
|
||||||
|
|
||||||
|
setFeedbackTitle("");
|
||||||
|
setFeedback("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
bottom={true}
|
||||||
|
height={"90%"}
|
||||||
|
width={width}
|
||||||
|
panDirection={PanningDirectionsEnum.DOWN}
|
||||||
|
onDismiss={() => addFeedbackProps.setIsVisible(false)}
|
||||||
|
containerStyle={styles.dialogContainer}
|
||||||
|
visible={addFeedbackProps.isVisible}
|
||||||
|
>
|
||||||
|
<View row spread style={styles.topBtns} marginB-20>
|
||||||
|
<Button
|
||||||
|
color="#05a8b6"
|
||||||
|
label="Cancel"
|
||||||
|
style={styles.topBtn}
|
||||||
|
onPress={() => {
|
||||||
|
addFeedbackProps.setIsVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity onPress={() => addFeedbackProps.setIsVisible(false)}>
|
||||||
|
<DropModalIcon style={{ marginTop: 15 }} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Button
|
||||||
|
color="#05a8b6"
|
||||||
|
label="Save"
|
||||||
|
style={styles.topBtn}
|
||||||
|
onPress={() => {
|
||||||
|
addFeedback({
|
||||||
|
id: 99,
|
||||||
|
|
||||||
|
title: feedbackTitle.trimEnd().trimStart(),
|
||||||
|
|
||||||
|
text: feedback.trimEnd().trimStart(),
|
||||||
|
});
|
||||||
|
addFeedbackProps.setIsVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View marginH-20>
|
||||||
|
<TextField
|
||||||
|
value={feedbackTitle}
|
||||||
|
ref={titleRef}
|
||||||
|
placeholder="Set Title"
|
||||||
|
text60R
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setFeedbackTitle(text);
|
||||||
|
}}
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
descriptionRef.current?.focus();
|
||||||
|
}}
|
||||||
|
style={styles.title}
|
||||||
|
blurOnSubmit={false}
|
||||||
|
returnKeyType="next"
|
||||||
|
/>
|
||||||
|
<View height={2} backgroundColor="#b3b3b3" width={"100%"} marginB-20 />
|
||||||
|
<TextField
|
||||||
|
ref={descriptionRef}
|
||||||
|
value={feedback}
|
||||||
|
placeholder="Write Description"
|
||||||
|
text70
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setFeedback(text);
|
||||||
|
}}
|
||||||
|
style={styles.description}
|
||||||
|
multiline
|
||||||
|
numberOfLines={4}
|
||||||
|
maxLength={255}
|
||||||
|
onEndEditing={() => {
|
||||||
|
descriptionRef.current?.blur();
|
||||||
|
}}
|
||||||
|
returnKeyType="done"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
dialogContainer: {
|
||||||
|
borderTopRightRadius: 15,
|
||||||
|
borderTopLeftRadius: 15,
|
||||||
|
backgroundColor: "white",
|
||||||
|
padding: 0,
|
||||||
|
paddingTop: 3,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
topBtns: {},
|
||||||
|
topBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
color: "#05a8b6",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontFamily: "Manrope_400Regular",
|
||||||
|
fontSize: 14,
|
||||||
|
textAlignVertical: "top",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default AddFeedback;
|
||||||
196
components/pages/feedback/EditFeedback.tsx
Normal file
196
components/pages/feedback/EditFeedback.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
TouchableOpacity,
|
||||||
|
TextFieldRef,
|
||||||
|
} from "react-native-ui-lib";
|
||||||
|
import { Dimensions, StyleSheet } from "react-native";
|
||||||
|
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
||||||
|
import PenIcon from "@/assets/svgs/PenIcon";
|
||||||
|
import BinIcon from "@/assets/svgs/BinIcon";
|
||||||
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
|
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
||||||
|
import NavCalendarIcon from "@/assets/svgs/NavCalendarIcon";
|
||||||
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
|
import RemindersIcon from "@/assets/svgs/RemindersIcon";
|
||||||
|
import MenuIcon from "@/assets/svgs/MenuIcon";
|
||||||
|
import { IFeedback, useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||||
|
import FeedbackDialog from "./FeedbackDialog";
|
||||||
|
|
||||||
|
const EditFeedback = (props: {
|
||||||
|
item: IFeedback;
|
||||||
|
isVisible: boolean;
|
||||||
|
setIsVisible: (value: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { updateFeedback, deleteFeedback } = useFeedbackContext();
|
||||||
|
const [text, setText] = useState<string>(props.item.text);
|
||||||
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
|
const textRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
|
const { width } = Dimensions.get("screen");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setText(props.item.text);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isVisible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
textRef?.current?.focus();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [props.isVisible]);
|
||||||
|
|
||||||
|
const showConfirmationDialog = () => {
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteNote = () => {
|
||||||
|
deleteFeedback(props.item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideConfirmationDialog = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
bottom={true}
|
||||||
|
height={"90%"}
|
||||||
|
width={width}
|
||||||
|
panDirection={PanningDirectionsEnum.DOWN}
|
||||||
|
onDismiss={() => props.setIsVisible(false)}
|
||||||
|
containerStyle={{
|
||||||
|
borderRadius: 15,
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: "100%",
|
||||||
|
alignSelf: "stretch",
|
||||||
|
padding: 10,
|
||||||
|
paddingTop: 3,
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
visible={props.isVisible}
|
||||||
|
>
|
||||||
|
<View center paddingT-8>
|
||||||
|
<TouchableOpacity onPress={() => props.setIsVisible(false)}>
|
||||||
|
<DropModalIcon />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<View row spread paddingH-10 paddingB-15>
|
||||||
|
<Button
|
||||||
|
color="#05a8b6"
|
||||||
|
style={styles.topBtn}
|
||||||
|
iconSource={() => <CloseXIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
props.setIsVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View row>
|
||||||
|
<Button
|
||||||
|
style={styles.topBtn}
|
||||||
|
marginR-10
|
||||||
|
iconSource={() => <PenIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
console.log("selview");
|
||||||
|
updateFeedback(props.item.id, { text: text });
|
||||||
|
props.setIsVisible(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
style={styles.topBtn}
|
||||||
|
marginL-5
|
||||||
|
iconSource={() => <BinIcon />}
|
||||||
|
onPress={() => {
|
||||||
|
showConfirmationDialog();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View centerH>
|
||||||
|
<Text style={styles.title}>{props.item.title} </Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.divider} />
|
||||||
|
<View row gap-5 paddingR-20>
|
||||||
|
<View paddingT-8 marginR-5>
|
||||||
|
<MenuIcon width={20} height={12} />
|
||||||
|
</View>
|
||||||
|
<TextField
|
||||||
|
textAlignVertical="top"
|
||||||
|
multiline
|
||||||
|
style={styles.description}
|
||||||
|
placeholder="Add description"
|
||||||
|
numberOfLines={3}
|
||||||
|
value={text}
|
||||||
|
onChangeText={(value) => {
|
||||||
|
setText(value);
|
||||||
|
}}
|
||||||
|
ref={textRef}
|
||||||
|
returnKeyType="done"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.divider} />
|
||||||
|
<FeedbackDialog
|
||||||
|
visible={modalVisible}
|
||||||
|
title={props.item.title}
|
||||||
|
onDismiss={hideConfirmationDialog}
|
||||||
|
onConfirm={handleDeleteNote}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
|
||||||
|
gradient: {
|
||||||
|
height: "25%",
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 25,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: "rgb(253, 23, 117)",
|
||||||
|
paddingVertical: 20,
|
||||||
|
},
|
||||||
|
topBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
color: "#05a8b6",
|
||||||
|
marginTop: -3,
|
||||||
|
},
|
||||||
|
rotateSwitch: {
|
||||||
|
marginLeft: 35,
|
||||||
|
marginBottom: 10,
|
||||||
|
marginTop: 25,
|
||||||
|
},
|
||||||
|
optionsReg: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
},
|
||||||
|
optionsBold: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
|
},
|
||||||
|
optionsIcon: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontFamily: "Manrope_400Regular",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default EditFeedback;
|
||||||
50
components/pages/feedback/Feedback.tsx
Normal file
50
components/pages/feedback/Feedback.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { View, Text } from "react-native-ui-lib";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
} from "react-native-gesture-handler";
|
||||||
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
import EditFeedback from "./EditFeedback";
|
||||||
|
|
||||||
|
const Feedback = (props: { item: IFeedback }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TouchableWithoutFeedback onPress={() => setIsVisible(true)}>
|
||||||
|
<View
|
||||||
|
backgroundColor="white"
|
||||||
|
marginV-5
|
||||||
|
paddingH-13
|
||||||
|
paddingV-10
|
||||||
|
style={{ borderRadius: 15, elevation: 2 }}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
text70B
|
||||||
|
style={{ fontSize: 15, fontFamily: "Manrope_600SemiBold" }}
|
||||||
|
marginB-8
|
||||||
|
>
|
||||||
|
{props.item.title}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
text70
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Manrope_400Regular",
|
||||||
|
color: "#5c5c5c",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.item.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
<EditFeedback
|
||||||
|
item={props.item}
|
||||||
|
isVisible={isVisible}
|
||||||
|
setIsVisible={setIsVisible}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Feedback;
|
||||||
81
components/pages/feedback/FeedbackDialog.tsx
Normal file
81
components/pages/feedback/FeedbackDialog.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
interface FeedbackDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedbackDialog: React.FC<FeedbackDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
onDismiss,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<Text center style={styles.title}>
|
||||||
|
Delete Note
|
||||||
|
</Text>
|
||||||
|
<View center>
|
||||||
|
<Text style={styles.text} center>
|
||||||
|
Are you sure you want to delete this feedback? {"\n\n"}
|
||||||
|
<Text style={{ fontSize: 16, fontFamily: "PlusJakartaSans_700Bold" }}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View row right gap-8>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={onDismiss}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={onConfirm}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stylesheet for future styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
confirmBtn: {
|
||||||
|
backgroundColor: "#ea156d",
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
paddingTop: 35,
|
||||||
|
paddingBottom: 17,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FeedbackDialog;
|
||||||
35
components/pages/feedback/FeedbackList.tsx
Normal file
35
components/pages/feedback/FeedbackList.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { View } from "react-native-ui-lib";
|
||||||
|
import React from "react";
|
||||||
|
import { FlatList } from "react-native";
|
||||||
|
import { useFeedbackContext } from "@/contexts/FeedbackContext";
|
||||||
|
import Feedback from "./Feedback";
|
||||||
|
|
||||||
|
const FeedbackList = (props: { searchText: string }) => {
|
||||||
|
const { feedbacks } = useFeedbackContext();
|
||||||
|
|
||||||
|
const filteredBrainDumps =
|
||||||
|
props.searchText.trim() === ""
|
||||||
|
? feedbacks
|
||||||
|
: feedbacks.filter(
|
||||||
|
(item) =>
|
||||||
|
item.title.toLowerCase().includes(props.searchText.toLowerCase()) ||
|
||||||
|
item.text
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(props.searchText.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View marginB-70>
|
||||||
|
<FlatList
|
||||||
|
style={{ zIndex: -1 }}
|
||||||
|
data={filteredBrainDumps}
|
||||||
|
keyExtractor={(item) => item.title}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Feedback key={item.title} item={item} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FeedbackList;
|
||||||
120
components/pages/feedback/FeedbackPage.tsx
Normal file
120
components/pages/feedback/FeedbackPage.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {Dimensions, ScrollView, StyleSheet} from "react-native";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||||
|
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||||
|
import {Feather, MaterialIcons} from "@expo/vector-icons";
|
||||||
|
import LinearGradient from "react-native-linear-gradient";
|
||||||
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
import AddFeedback from "./AddFeedback";
|
||||||
|
import FeedbackList from "./FeedbackList";
|
||||||
|
|
||||||
|
const FeedbackPage = () => {
|
||||||
|
const [searchText, setSearchText] = useState<string>("");
|
||||||
|
const [isAddVisible, setIsAddVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View height={"100%"}>
|
||||||
|
<View>
|
||||||
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View marginH-25>
|
||||||
|
<HeaderTemplate
|
||||||
|
message={"Welcome to your Feedback!"}
|
||||||
|
isWelcome={false}
|
||||||
|
children={
|
||||||
|
<Text
|
||||||
|
style={{fontFamily: "Manrope_400Regular", fontSize: 14}}
|
||||||
|
>
|
||||||
|
Drop your feedback here, and{"\n"}organize it later.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<View>
|
||||||
|
<View style={styles.searchField} centerV>
|
||||||
|
<TextField
|
||||||
|
value={searchText}
|
||||||
|
onChangeText={(value) => {
|
||||||
|
setSearchText(value);
|
||||||
|
}}
|
||||||
|
leadingAccessory={
|
||||||
|
<Feather
|
||||||
|
name="search"
|
||||||
|
size={24}
|
||||||
|
color="#9b9b9b"
|
||||||
|
style={{paddingRight: 10}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
fontSize: 15,
|
||||||
|
}}
|
||||||
|
placeholder="Search your feedbacks..."
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<FeedbackList searchText={searchText}/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
<LinearGradient
|
||||||
|
colors={["#f9f8f700", "#f9f8f7"]}
|
||||||
|
locations={[0,1]}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
height: 120,
|
||||||
|
width: Dimensions.get("screen").width,
|
||||||
|
justifyContent:'center',
|
||||||
|
alignItems:"center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
position: "relative",
|
||||||
|
width: "90%",
|
||||||
|
bottom: -10,
|
||||||
|
borderRadius: 30,
|
||||||
|
backgroundColor: "#fd1775",
|
||||||
|
}}
|
||||||
|
color="white"
|
||||||
|
enableShadow
|
||||||
|
onPress={() => {
|
||||||
|
setIsAddVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View row centerV centerH>
|
||||||
|
<PlusIcon />
|
||||||
|
<Text
|
||||||
|
white
|
||||||
|
style={{fontSize: 16, fontFamily: "Manrope_600SemiBold", marginLeft: 5}}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Button>
|
||||||
|
</LinearGradient>
|
||||||
|
<AddFeedback
|
||||||
|
addFeedbackProps={{
|
||||||
|
isVisible: isAddVisible,
|
||||||
|
setIsVisible: setIsAddVisible,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
searchField: {
|
||||||
|
borderWidth: 0.7,
|
||||||
|
borderColor: "#9b9b9b",
|
||||||
|
borderRadius: 15,
|
||||||
|
height: 42,
|
||||||
|
paddingLeft: 10,
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FeedbackPage;
|
||||||
@ -1,40 +1,25 @@
|
|||||||
import { StyleSheet } from "react-native";
|
import {Dimensions, StyleSheet} from "react-native";
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import {
|
import {Button, View,} from "react-native-ui-lib";
|
||||||
Button,
|
import {useGroceryContext} from "@/contexts/GroceryContext";
|
||||||
Colors,
|
import {FontAwesome6} from "@expo/vector-icons";
|
||||||
Dialog,
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
Drawer,
|
|
||||||
Text,
|
const { width } = Dimensions.get("screen");
|
||||||
View,
|
|
||||||
PanningProvider,
|
const AddGroceryItem = () => {
|
||||||
} from "react-native-ui-lib";
|
const {setIsAddingGrocery} = useGroceryContext();
|
||||||
import { useGroceryContext } from "@/contexts/GroceryContext";
|
|
||||||
import { FontAwesome6 } from "@expo/vector-icons";
|
|
||||||
interface AddGroceryItemProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
const AddGroceryItem = () => {
|
|
||||||
const { isAddingGrocery, setIsAddingGrocery } = useGroceryContext();
|
|
||||||
const [visible, setVisible] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleShowDialog = () => {
|
|
||||||
setVisible(true);
|
|
||||||
};
|
|
||||||
const handleHideDialog = () => {
|
|
||||||
setVisible(false);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
row
|
row
|
||||||
spread
|
spread
|
||||||
paddingH-25
|
paddingH-20
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 20,
|
bottom: 15,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 60,
|
height: 53.26,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={styles.btnContainer} row>
|
<View style={styles.btnContainer} row>
|
||||||
@ -43,7 +28,7 @@ const AddGroceryItem = () => {
|
|||||||
backgroundColor="#fd1775"
|
backgroundColor="#fd1775"
|
||||||
label="Add item"
|
label="Add item"
|
||||||
text70L
|
text70L
|
||||||
iconSource={() => <FontAwesome6 name="add" size={18} color="white" />}
|
iconSource={() => <PlusIcon />}
|
||||||
style={styles.finishShopBtn}
|
style={styles.finishShopBtn}
|
||||||
labelStyle={styles.addBtnLbl}
|
labelStyle={styles.addBtnLbl}
|
||||||
enableShadow
|
enableShadow
|
||||||
@ -82,15 +67,22 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
},
|
},
|
||||||
btnContainer: {
|
btnContainer: {
|
||||||
width: "100%",
|
position:"absolute",
|
||||||
|
bottom: 30,
|
||||||
|
width: width,
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 0,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
alignItems:"center",
|
||||||
|
zIndex: 10,
|
||||||
},
|
},
|
||||||
finishShopBtn: {
|
finishShopBtn: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
height: 53.26,
|
||||||
},
|
},
|
||||||
shoppingBtn: {
|
shoppingBtn: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginHorizontal: 3,
|
marginHorizontal: 3,
|
||||||
},
|
},
|
||||||
addBtnLbl: { fontFamily: "Manrope_500Medium", fontSize: 17, marginLeft: 5 },
|
addBtnLbl: {fontFamily: "Manrope_500Medium", fontSize: 17, marginLeft: 5},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Text, View } from "react-native-ui-lib";
|
import { Text, TextField, TextFieldRef, View } from "react-native-ui-lib";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { TextField, TextFieldRef } from "react-native-ui-lib";
|
|
||||||
import { GroceryCategory, useGroceryContext } from "@/contexts/GroceryContext";
|
import { GroceryCategory, useGroceryContext } from "@/contexts/GroceryContext";
|
||||||
import { Dropdown } from "react-native-element-dropdown";
|
import { Dropdown } from "react-native-element-dropdown";
|
||||||
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
import CloseXIcon from "@/assets/svgs/CloseXIcon";
|
||||||
import { StyleSheet } from "react-native";
|
import { findNodeHandle, StyleSheet, UIManager } from "react-native";
|
||||||
import DropdownIcon from "@/assets/svgs/DropdownIcon";
|
import DropdownIcon from "@/assets/svgs/DropdownIcon";
|
||||||
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
|
|
||||||
interface IEditGrocery {
|
interface IEditGrocery {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -14,13 +14,41 @@ interface IEditGrocery {
|
|||||||
setTitle: (value: string) => void;
|
setTitle: (value: string) => void;
|
||||||
setCategory?: (category: GroceryCategory) => void;
|
setCategory?: (category: GroceryCategory) => void;
|
||||||
setSubmit?: (value: boolean) => void;
|
setSubmit?: (value: boolean) => void;
|
||||||
closeEdit?: (value: boolean) => void;
|
closeEdit?: () => void;
|
||||||
handleEditSubmit?: Function;
|
handleEditSubmit?: Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
|
const EditGroceryItem = ({
|
||||||
|
editGrocery,
|
||||||
|
onInputFocus,
|
||||||
|
}: {
|
||||||
|
editGrocery: IEditGrocery;
|
||||||
|
onInputFocus: (y: number) => void;
|
||||||
|
}) => {
|
||||||
const { fuzzyMatchGroceryCategory } = useGroceryContext();
|
const { fuzzyMatchGroceryCategory } = useGroceryContext();
|
||||||
const inputRef = useRef<TextFieldRef>(null);
|
const inputRef = useRef<TextFieldRef>(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
const handle = findNodeHandle(containerRef.current);
|
||||||
|
if (handle) {
|
||||||
|
UIManager.measure(
|
||||||
|
handle,
|
||||||
|
(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
pageX: number,
|
||||||
|
pageY: number
|
||||||
|
) => {
|
||||||
|
onInputFocus(pageY);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const groceryCategoryOptions = Object.values(GroceryCategory).map(
|
const groceryCategoryOptions = Object.values(GroceryCategory).map(
|
||||||
(category) => ({
|
(category) => ({
|
||||||
@ -29,15 +57,35 @@ const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
inputRef?.current?.blur();
|
||||||
|
console.log("CALLLLLL");
|
||||||
|
if (editGrocery.setSubmit) {
|
||||||
|
editGrocery.setSubmit(true);
|
||||||
|
}
|
||||||
|
if (editGrocery.handleEditSubmit) {
|
||||||
|
editGrocery.handleEditSubmit({
|
||||||
|
id: editGrocery.id,
|
||||||
|
title: editGrocery.title,
|
||||||
|
category: editGrocery.category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (editGrocery.closeEdit) {
|
||||||
|
editGrocery.closeEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
inputRef.current.focus(); // Focus on the TextField
|
inputRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(editGrocery.category);
|
console.log(editGrocery.category);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
ref={containerRef}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -50,48 +98,53 @@ const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
|
|||||||
<View row spread centerV>
|
<View row spread centerV>
|
||||||
<TextField
|
<TextField
|
||||||
text70T
|
text70T
|
||||||
style={{}}
|
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
onFocus={handleFocus}
|
||||||
placeholder="Grocery"
|
placeholder="Grocery"
|
||||||
value={editGrocery.title}
|
value={editGrocery.title}
|
||||||
|
onSubmitEditing={handleSubmit}
|
||||||
|
numberOfLines={1}
|
||||||
|
returnKeyType="done"
|
||||||
onChangeText={(value) => {
|
onChangeText={(value) => {
|
||||||
editGrocery.setTitle(value);
|
editGrocery.setTitle(value);
|
||||||
}}
|
let groceryCategory = fuzzyMatchGroceryCategory(value);
|
||||||
onSubmitEditing={() => {
|
if (editGrocery.setCategory) {
|
||||||
if (editGrocery.setSubmit) {
|
editGrocery.setCategory(groceryCategory);
|
||||||
editGrocery.setSubmit(true);
|
|
||||||
}
|
|
||||||
if (editGrocery.handleEditSubmit) {
|
|
||||||
editGrocery.handleEditSubmit({
|
|
||||||
id: editGrocery.id,
|
|
||||||
title: editGrocery.title,
|
|
||||||
category: editGrocery.category,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (editGrocery.closeEdit) {
|
|
||||||
editGrocery.closeEdit(false);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
maxLength={25}
|
maxLength={25}
|
||||||
/>
|
/>
|
||||||
|
<View row centerV>
|
||||||
|
<AntDesign
|
||||||
|
name="check"
|
||||||
|
size={24}
|
||||||
|
style={{
|
||||||
|
color: "green",
|
||||||
|
marginRight: 15,
|
||||||
|
}}
|
||||||
|
onPress={handleSubmit}
|
||||||
|
/>
|
||||||
<CloseXIcon
|
<CloseXIcon
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (editGrocery.closeEdit) editGrocery.closeEdit(false);
|
if (editGrocery.closeEdit) {
|
||||||
|
editGrocery.closeEdit();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
style={{marginTop: 15}}
|
style={{ marginTop: 15 }}
|
||||||
data={groceryCategoryOptions}
|
data={groceryCategoryOptions}
|
||||||
placeholder="Select grocery category"
|
placeholder="Select grocery category"
|
||||||
placeholderStyle={{ color: "#a2a2a2", fontFamily: "Manrope_500Medium", fontSize: 13.2 }}
|
placeholderStyle={{
|
||||||
|
color: "#a2a2a2",
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
fontSize: 13.2,
|
||||||
|
}}
|
||||||
labelField="label"
|
labelField="label"
|
||||||
valueField="value"
|
valueField="value"
|
||||||
value={
|
value={editGrocery.category}
|
||||||
editGrocery.category == GroceryCategory.None
|
|
||||||
? null
|
|
||||||
: editGrocery.category
|
|
||||||
}
|
|
||||||
iconColor="white"
|
iconColor="white"
|
||||||
activeColor={"#fd1775"}
|
activeColor={"#fd1775"}
|
||||||
containerStyle={styles.dropdownStyle}
|
containerStyle={styles.dropdownStyle}
|
||||||
@ -99,7 +152,14 @@ const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
|
|||||||
itemContainerStyle={styles.itemStyle}
|
itemContainerStyle={styles.itemStyle}
|
||||||
selectedTextStyle={styles.selectedText}
|
selectedTextStyle={styles.selectedText}
|
||||||
renderLeftIcon={() => (
|
renderLeftIcon={() => (
|
||||||
<DropdownIcon style={{ marginRight: 8 }} color={editGrocery.category == GroceryCategory.None ? "#7b7b7b" : "#fd1775"} />
|
<DropdownIcon
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
color={
|
||||||
|
editGrocery.category == GroceryCategory.None
|
||||||
|
? "#7b7b7b"
|
||||||
|
: "#fd1775"
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
renderItem={(item) => {
|
renderItem={(item) => {
|
||||||
return (
|
return (
|
||||||
@ -114,8 +174,11 @@ const EditGroceryItem = ({ editGrocery }: { editGrocery: IEditGrocery }) => {
|
|||||||
id: editGrocery.id,
|
id: editGrocery.id,
|
||||||
category: item.value,
|
category: item.value,
|
||||||
});
|
});
|
||||||
console.log("kategorija vo diropdown: " + item.value);
|
if (editGrocery.closeEdit) editGrocery.closeEdit();
|
||||||
if (editGrocery.closeEdit) editGrocery.closeEdit(false);
|
} else {
|
||||||
|
if (editGrocery.setCategory) {
|
||||||
|
editGrocery.setCategory(item.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,24 +8,33 @@ import { ImageBackground, StyleSheet } from "react-native";
|
|||||||
import { IGrocery } from "@/hooks/firebase/types/groceryData";
|
import { IGrocery } from "@/hooks/firebase/types/groceryData";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||||
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
const GroceryItem = ({
|
const GroceryItem = ({
|
||||||
item,
|
item,
|
||||||
handleItemApproved,
|
handleItemApproved,
|
||||||
|
onInputFocus,
|
||||||
}: {
|
}: {
|
||||||
item: IGrocery;
|
item: IGrocery;
|
||||||
handleItemApproved: (id: string, changes: Partial<IGrocery>) => void;
|
handleItemApproved: (id: string, changes: Partial<IGrocery>) => void;
|
||||||
|
onInputFocus: (y: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { updateGroceryItem } = useGroceryContext();
|
const { updateGroceryItem } = useGroceryContext();
|
||||||
|
const { profileData } = useAuthContext();
|
||||||
|
const isParent = profileData?.userType === ProfileType.PARENT;
|
||||||
|
|
||||||
const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false);
|
const [openFreqEdit, setOpenFreqEdit] = useState<boolean>(false);
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false);
|
const [isEditingTitle, setIsEditingTitle] = useState<boolean>(false);
|
||||||
const [newTitle, setNewTitle] = useState<string>("");
|
const [newTitle, setNewTitle] = useState<string>(item.title ?? "");
|
||||||
const [category, setCategory] = useState<GroceryCategory>(
|
const [category, setCategory] = useState<GroceryCategory>(
|
||||||
GroceryCategory.None
|
item.category ?? GroceryCategory.None
|
||||||
);
|
);
|
||||||
const [itemCreator, setItemCreator] = useState<UserProfile>(null);
|
const [itemCreator, setItemCreator] = useState<UserProfile>(null);
|
||||||
|
|
||||||
|
const closeEdit = () => {
|
||||||
|
setIsEditingTitle(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleTitleChange = (newTitle: string) => {
|
const handleTitleChange = (newTitle: string) => {
|
||||||
updateGroceryItem({ id: item?.id, title: newTitle });
|
updateGroceryItem({ id: item?.id, title: newTitle });
|
||||||
};
|
};
|
||||||
@ -35,7 +44,6 @@ const GroceryItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNewTitle(item.title);
|
|
||||||
console.log(item);
|
console.log(item);
|
||||||
getItemCreator(item?.creatorId);
|
getItemCreator(item?.creatorId);
|
||||||
}, []);
|
}, []);
|
||||||
@ -53,6 +61,10 @@ const GroceryItem = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInitials = (firstName: string, lastName: string) => {
|
||||||
|
return `${firstName.charAt(0)}${lastName.charAt(0)}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@ -61,6 +73,8 @@ const GroceryItem = ({
|
|||||||
marginVertical: 5,
|
marginVertical: 5,
|
||||||
paddingHorizontal: isEditingTitle ? 0 : 13,
|
paddingHorizontal: isEditingTitle ? 0 : 13,
|
||||||
paddingVertical: isEditingTitle ? 0 : 10,
|
paddingVertical: isEditingTitle ? 0 : 10,
|
||||||
|
height: 44.64,
|
||||||
|
backgroundColor: item.bought ? "#cbcbcb" : "white",
|
||||||
}}
|
}}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
centerV
|
centerV
|
||||||
@ -74,54 +88,83 @@ const GroceryItem = ({
|
|||||||
setOpenFreqEdit(false);
|
setOpenFreqEdit(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isEditingTitle ? (
|
{isEditingTitle ? (
|
||||||
<View>
|
|
||||||
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
|
|
||||||
<Text text70T black style={styles.title}>
|
|
||||||
{item.title}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<EditGroceryItem
|
<EditGroceryItem
|
||||||
editGrocery={{
|
editGrocery={{
|
||||||
id: item.id,
|
id: item.id,
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
category: item.category,
|
category: category,
|
||||||
setTitle: setNewTitle,
|
setTitle: setNewTitle,
|
||||||
setCategory: setCategory,
|
setCategory: setCategory,
|
||||||
closeEdit: setIsEditingTitle,
|
closeEdit: closeEdit,
|
||||||
handleEditSubmit: updateGroceryItem,
|
handleEditSubmit: updateGroceryItem,
|
||||||
}}
|
}}
|
||||||
|
onInputFocus={onInputFocus}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
{isParent ? (
|
||||||
|
<TouchableOpacity onPress={() => setIsEditingTitle(true)}>
|
||||||
|
<Text
|
||||||
|
text70T
|
||||||
|
black
|
||||||
|
style={[
|
||||||
|
styles.title,
|
||||||
|
{
|
||||||
|
textDecorationLine: item.bought ? "line-through" : "none",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<Text
|
||||||
|
text70T
|
||||||
|
black
|
||||||
|
style={[styles.title, { color: item.bought ? "red" : "black" }]}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
{!item.approved ? (
|
{!item.approved ? (
|
||||||
<View row centerV marginB-10>
|
<View row centerV marginB-10>
|
||||||
|
{isParent && (
|
||||||
|
<>
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name="check"
|
name="check"
|
||||||
size={24}
|
size={24}
|
||||||
style={{
|
style={{
|
||||||
color: item.approved ? "green" : "#aaaaaa",
|
color: "green",
|
||||||
marginRight: 15,
|
marginRight: 15,
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={
|
||||||
handleItemApproved(item.id, { approved: true });
|
isParent
|
||||||
}}
|
? () => handleItemApproved(item.id, { approved: true })
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name="close"
|
name="close"
|
||||||
size={24}
|
size={24}
|
||||||
style={{ color: item.approved ? "#aaaaaa" : "red" }}
|
style={{ color: "red" }}
|
||||||
onPress={() => {
|
onPress={
|
||||||
handleItemApproved(item.id, { approved: false });
|
isParent
|
||||||
}}
|
? () => handleItemApproved(item.id, { approved: false })
|
||||||
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
!isEditingTitle && (
|
!isEditingTitle &&
|
||||||
|
isParent && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={item.bought}
|
value={item.bought}
|
||||||
containerStyle={[styles.checkbox, {borderRadius: 50}]}
|
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
|
||||||
style={styles.checked}
|
style={styles.checked}
|
||||||
borderRadius={50}
|
borderRadius={50}
|
||||||
color="#fd1575"
|
color="#fd1575"
|
||||||
@ -139,17 +182,49 @@ const GroceryItem = ({
|
|||||||
<View height={0.7} backgroundColor="#e7e7e7" width={"98%"} />
|
<View height={0.7} backgroundColor="#e7e7e7" width={"98%"} />
|
||||||
</View>
|
</View>
|
||||||
<View paddingL-0 paddingT-12 flexS row centerV>
|
<View paddingL-0 paddingT-12 flexS row centerV>
|
||||||
|
{profileData?.pfp ? (
|
||||||
<ImageBackground
|
<ImageBackground
|
||||||
|
source={require("../../../assets/images/child-picture.png")}
|
||||||
style={{
|
style={{
|
||||||
width: 22.36,
|
height: 24.64,
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
borderRadius: 50,
|
borderRadius: 22,
|
||||||
backgroundColor: "red",
|
|
||||||
marginRight: 10,
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
source={require("../../../assets/images/child-picture.png")}
|
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: 24.64,
|
||||||
|
aspectRatio: 1,
|
||||||
|
marginRight: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ccc",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: 100, // Circular shape
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{itemCreator
|
||||||
|
? getInitials(itemCreator.firstName, itemCreator.lastName)
|
||||||
|
: ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
<Text color="#858585" style={styles.authorTxt}>
|
<Text color="#858585" style={styles.authorTxt}>
|
||||||
Requested by {itemCreator?.firstName}
|
Requested by {itemCreator?.firstName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
import { FlatList, StyleSheet } from "react-native";
|
import {FlatList, StyleSheet} from "react-native";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import { Button, Text, TouchableOpacity, View } from "react-native-ui-lib";
|
import {Text, TouchableOpacity, View} from "react-native-ui-lib";
|
||||||
import GroceryItem from "./GroceryItem";
|
import GroceryItem from "./GroceryItem";
|
||||||
import {
|
import {GroceryCategory, GroceryFrequency, useGroceryContext,} from "@/contexts/GroceryContext";
|
||||||
GroceryCategory,
|
|
||||||
GroceryFrequency,
|
|
||||||
useGroceryContext,
|
|
||||||
} from "@/contexts/GroceryContext";
|
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||||
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
|
import {AntDesign} from "@expo/vector-icons";
|
||||||
import EditGroceryItem from "./EditGroceryItem";
|
import EditGroceryItem from "./EditGroceryItem";
|
||||||
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import { IGrocery } from "@/hooks/firebase/types/groceryData";
|
import {IGrocery} from "@/hooks/firebase/types/groceryData";
|
||||||
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
|
import AddPersonIcon from "@/assets/svgs/AddPersonIcon";
|
||||||
|
|
||||||
const GroceryList = () => {
|
const GroceryList = ({onInputFocus}: {onInputFocus: (y: number) => void}) => {
|
||||||
const {
|
const {
|
||||||
groceries,
|
groceries,
|
||||||
updateGroceryItem,
|
updateGroceryItem,
|
||||||
@ -22,16 +18,17 @@ const GroceryList = () => {
|
|||||||
setIsAddingGrocery,
|
setIsAddingGrocery,
|
||||||
addGrocery,
|
addGrocery,
|
||||||
} = useGroceryContext();
|
} = useGroceryContext();
|
||||||
const { profileData } = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>(
|
const [approvedGroceries, setapprovedGroceries] = useState<IGrocery[]>(
|
||||||
groceries?.filter((item) => item.approved === true)
|
groceries?.filter((item) => item.approved)
|
||||||
);
|
);
|
||||||
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
|
const [pendingGroceries, setPendingGroceries] = useState<IGrocery[]>(
|
||||||
groceries?.filter((item) => item.approved !== true)
|
groceries?.filter((item) => !item.approved)
|
||||||
);
|
);
|
||||||
const [category, setCategory] = useState<GroceryCategory>(
|
const [category, setCategory] = useState<GroceryCategory>(
|
||||||
GroceryCategory.None
|
GroceryCategory.None
|
||||||
);
|
);
|
||||||
|
|
||||||
const [title, setTitle] = useState<string>("");
|
const [title, setTitle] = useState<string>("");
|
||||||
const [submit, setSubmitted] = useState<boolean>(false);
|
const [submit, setSubmitted] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -72,19 +69,16 @@ const GroceryList = () => {
|
|||||||
}, [submit]);
|
}, [submit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/**/
|
setapprovedGroceries(groceries?.filter((item) => item.approved));
|
||||||
}, [category]);
|
setPendingGroceries(groceries?.filter((item) => !item.approved));
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setapprovedGroceries(groceries?.filter((item) => item.approved === true));
|
|
||||||
setPendingGroceries(groceries?.filter((item) => item.approved !== true));
|
|
||||||
}, [groceries]);
|
}, [groceries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View marginH-20 marginB-20>
|
<View marginH-20 marginB-45>
|
||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message={"Welcome to your grocery list"}
|
message={"Welcome to your grocery list"}
|
||||||
isWelcome={false}
|
isWelcome={false}
|
||||||
|
isGroceries={true}
|
||||||
>
|
>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<View
|
<View
|
||||||
@ -93,7 +87,7 @@ const GroceryList = () => {
|
|||||||
paddingV-8
|
paddingV-8
|
||||||
marginR-5
|
marginR-5
|
||||||
centerV
|
centerV
|
||||||
style={{ borderRadius: 50 }}
|
style={{borderRadius: 50}}
|
||||||
>
|
>
|
||||||
<Text text70BL color="#46a80a" style={styles.counterText}>
|
<Text text70BL color="#46a80a" style={styles.counterText}>
|
||||||
{approvedGroceries?.length} list{" "}
|
{approvedGroceries?.length} list{" "}
|
||||||
@ -113,7 +107,7 @@ const GroceryList = () => {
|
|||||||
padding-8
|
padding-8
|
||||||
paddingH-12
|
paddingH-12
|
||||||
marginR-15
|
marginR-15
|
||||||
style={{ borderRadius: 50 }}
|
style={{borderRadius: 50}}
|
||||||
>
|
>
|
||||||
<Text text70 style={styles.counterText} color="#e28800">
|
<Text text70 style={styles.counterText} color="#e28800">
|
||||||
{pendingGroceries?.length} pending
|
{pendingGroceries?.length} pending
|
||||||
@ -170,12 +164,13 @@ const GroceryList = () => {
|
|||||||
? pendingVisible && (
|
? pendingVisible && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={pendingGroceries}
|
data={pendingGroceries}
|
||||||
renderItem={({ item }) => (
|
renderItem={({item}) => (
|
||||||
<GroceryItem
|
<GroceryItem
|
||||||
item={item}
|
item={item}
|
||||||
handleItemApproved={(id, changes) =>
|
handleItemApproved={(id, changes) =>
|
||||||
updateGroceryItem({ ...changes, id: id })
|
updateGroceryItem({...changes, id: id})
|
||||||
}
|
}
|
||||||
|
onInputFocus={onInputFocus}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
@ -227,6 +222,7 @@ const GroceryList = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{isAddingGrocery && (
|
{isAddingGrocery && (
|
||||||
|
<View style={{marginTop: 8}}>
|
||||||
<EditGroceryItem
|
<EditGroceryItem
|
||||||
editGrocery={{
|
editGrocery={{
|
||||||
title: title,
|
title: title,
|
||||||
@ -234,8 +230,11 @@ const GroceryList = () => {
|
|||||||
category: category,
|
category: category,
|
||||||
setTitle: setTitle,
|
setTitle: setTitle,
|
||||||
setSubmit: setSubmitted,
|
setSubmit: setSubmitted,
|
||||||
|
closeEdit: () => setIsAddingGrocery(false)
|
||||||
}}
|
}}
|
||||||
|
onInputFocus={onInputFocus}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Render Approved Groceries Grouped by Category */}
|
{/* Render Approved Groceries Grouped by Category */}
|
||||||
@ -243,10 +242,10 @@ const GroceryList = () => {
|
|||||||
? approvedVisible && (
|
? approvedVisible && (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={Object.keys(approvedGroceriesByCategory)}
|
data={Object.keys(approvedGroceriesByCategory)}
|
||||||
renderItem={({ item: category }) => (
|
renderItem={({item: category}) => (
|
||||||
<View key={category}>
|
<View key={category}>
|
||||||
{/* Render Category Header */}
|
{/* Render Category Header */}
|
||||||
<Text text80M style={{ marginTop: 10 }} color="#666">
|
<Text text80M style={{marginTop: 10}} color="#666">
|
||||||
{category}
|
{category}
|
||||||
</Text>
|
</Text>
|
||||||
{/* Render Grocery Items for this Category */}
|
{/* Render Grocery Items for this Category */}
|
||||||
@ -256,8 +255,9 @@ const GroceryList = () => {
|
|||||||
key={grocery.id}
|
key={grocery.id}
|
||||||
item={grocery}
|
item={grocery}
|
||||||
handleItemApproved={(id, changes) =>
|
handleItemApproved={(id, changes) =>
|
||||||
updateGroceryItem({ ...changes, id: id })
|
updateGroceryItem({...changes, id: id})
|
||||||
}
|
}
|
||||||
|
onInputFocus={onInputFocus}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,37 +1,78 @@
|
|||||||
import { Text, ScrollView } from "react-native";
|
import { Dimensions, ScrollView, Keyboard, Platform } from "react-native";
|
||||||
import { View } from "react-native-ui-lib";
|
import { View } from "react-native-ui-lib";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import AddGroceryItem from "./AddGroceryItem";
|
import AddGroceryItem from "./AddGroceryItem";
|
||||||
import GroceryList from "./GroceryList";
|
import GroceryList from "./GroceryList";
|
||||||
import { useGroceryContext } from "@/contexts/GroceryContext";
|
import { useGroceryContext } from "@/contexts/GroceryContext";
|
||||||
|
|
||||||
const GroceryWrapper = () => {
|
const GroceryWrapper = () => {
|
||||||
const { isAddingGrocery } = useGroceryContext();
|
const { isAddingGrocery } = useGroceryContext();
|
||||||
const scrollViewRef = useRef<ScrollView>(null); // Reference to the ScrollView
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const keyboardWillShowListener = Keyboard.addListener(
|
||||||
|
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
|
||||||
|
(e) => {
|
||||||
|
setKeyboardHeight(e.endCoordinates.height);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyboardWillHideListener = Keyboard.addListener(
|
||||||
|
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
|
||||||
|
() => {
|
||||||
|
setKeyboardHeight(0);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
keyboardWillShowListener.remove();
|
||||||
|
keyboardWillHideListener.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAddingGrocery && scrollViewRef.current) {
|
if (isAddingGrocery && scrollViewRef.current) {
|
||||||
scrollViewRef.current.scrollTo({
|
scrollViewRef.current.scrollTo({
|
||||||
y: 400, // Adjust this value to scroll a bit down (100 is an example)
|
y: 400,
|
||||||
animated: true,
|
animated: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isAddingGrocery]);
|
}, [isAddingGrocery]);
|
||||||
|
|
||||||
|
const handleInputFocus = (y: number) => {
|
||||||
|
if (scrollViewRef.current) {
|
||||||
|
// Get the window height
|
||||||
|
const windowHeight = Dimensions.get('window').height;
|
||||||
|
// Calculate the space we want to leave at the top
|
||||||
|
const topSpacing = 20;
|
||||||
|
|
||||||
|
// Calculate the target scroll position:
|
||||||
|
// y (position of input) - topSpacing (space we want at top)
|
||||||
|
// if keyboard is shown, we need to account for its height
|
||||||
|
const scrollPosition = Math.max(0, y - topSpacing);
|
||||||
|
|
||||||
|
scrollViewRef.current.scrollTo({
|
||||||
|
y: scrollPosition,
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View height={"100%"} paddingT-15 paddingH-15>
|
<>
|
||||||
<View height={"100%"}>
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
ref={scrollViewRef} // Assign the ref to the ScrollView
|
ref={scrollViewRef}
|
||||||
automaticallyAdjustKeyboardInsets={true}
|
automaticallyAdjustKeyboardInsets={true}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View marginB-70>
|
<View marginB-60>
|
||||||
<GroceryList />
|
<GroceryList onInputFocus={handleInputFocus} />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{!isAddingGrocery && <AddGroceryItem />}
|
{!isAddingGrocery && <AddGroceryItem />}
|
||||||
</View>
|
</>
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ const Entry = () => {
|
|||||||
const [tab, setTab] = useState<"register" | "login" | "reset-password">("login");
|
const [tab, setTab] = useState<"register" | "login" | "reset-password">("login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={{height:"100%"}}>
|
||||||
{tab === "register" && <SignUpPage setTab={setTab}/>}
|
{tab === "register" && <SignUpPage setTab={setTab}/>}
|
||||||
{tab === "login" && <SignInPage setTab={setTab}/>}
|
{tab === "login" && <SignInPage setTab={setTab}/>}
|
||||||
{tab === "reset-password" && <ResetPasswordPage setTab={setTab}/>}
|
{tab === "reset-password" && <ResetPasswordPage setTab={setTab}/>}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import {Button, ButtonSize, Text, TextField, View} from "react-native-ui-lib";
|
import {Button, Text, TextField, View} from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from "react-native";
|
||||||
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
|
import {useResetPassword} from "@/hooks/firebase/useResetPassword";
|
||||||
import {isLoading} from "expo-font";
|
|
||||||
|
|
||||||
export const ResetPasswordPage = ({setTab}: { setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">> }) => {
|
export const ResetPasswordPage = () => {
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
|
|
||||||
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
const {mutateAsync: resetPassword, error, isError, isLoading} = useResetPassword();
|
||||||
|
|||||||
@ -1,159 +1,170 @@
|
|||||||
import {Button, ButtonSize, Dialog, Text, TextField, View} from "react-native-ui-lib";
|
import {
|
||||||
import React, {useState} from "react";
|
Button,
|
||||||
import {useSignIn} from "@/hooks/firebase/useSignIn";
|
ButtonSize,
|
||||||
import {StyleSheet} from "react-native";
|
Colors,
|
||||||
import Toast from 'react-native-toast-message';
|
KeyboardAwareScrollView,
|
||||||
import {useLoginWithQrCode} from "@/hooks/firebase/useLoginWithQrCode";
|
LoaderScreen,
|
||||||
import {Camera, CameraView} from 'expo-camera';
|
Text,
|
||||||
|
TextField,
|
||||||
|
TextFieldRef,
|
||||||
|
View,
|
||||||
|
} from "react-native-ui-lib";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { useSignIn } from "@/hooks/firebase/useSignIn";
|
||||||
|
import { KeyboardAvoidingView, Platform, StyleSheet } from "react-native";
|
||||||
|
import Toast from "react-native-toast-message";
|
||||||
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
const SignInPage = ({setTab}: {
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
||||||
setTab: React.Dispatch<React.SetStateAction<"register" | "login" | "reset-password">>
|
|
||||||
}) => {
|
const SignInPage = () => {
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>("");
|
||||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
const passwordRef = useRef<TextFieldRef>(null);
|
||||||
const [showCameraDialog, setShowCameraDialog] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const {mutateAsync: signIn, error, isError} = useSignIn();
|
const { mutateAsync: signIn, error, isError, isLoading } = useSignIn();
|
||||||
const {mutateAsync: signInWithQrCode} = useLoginWithQrCode()
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSignIn = async () => {
|
const handleSignIn = async () => {
|
||||||
await signIn({email, password});
|
await signIn({ email, password });
|
||||||
if (!isError) {
|
if (!isError) {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "success",
|
type: "success",
|
||||||
text1: "Login successful!"
|
text1: "Login successful!",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
Toast.show({
|
||||||
type: "error",
|
type: "error",
|
||||||
text1: "Error logging in",
|
text1: "Error logging in",
|
||||||
text2: `${error}`
|
text2: `${error}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQrCodeScanned = async ({data}: { data: string }) => {
|
|
||||||
setShowCameraDialog(false);
|
|
||||||
try {
|
|
||||||
await signInWithQrCode({userId: data});
|
|
||||||
Toast.show({
|
|
||||||
type: "success",
|
|
||||||
text1: "Login successful with QR code!"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Toast.show({
|
|
||||||
type: "error",
|
|
||||||
text1: "Error logging in with QR code",
|
|
||||||
text2: `${err}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCameraPermissions = async (callback: () => void) => {
|
|
||||||
const {status} = await Camera.requestCameraPermissionsAsync();
|
|
||||||
setHasPermission(status === 'granted');
|
|
||||||
if (status === 'granted') {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View padding-10 centerV height={"100%"}>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
|
<KeyboardAwareScrollView
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
|
enableOnAndroid
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{ flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%" }}
|
||||||
|
>
|
||||||
|
<View gap-13 width={"100%"} marginB-20>
|
||||||
|
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
|
||||||
|
Jump back into Cally
|
||||||
|
</Text>
|
||||||
|
<Text color={"#919191"} style={{ fontSize: 20 }}>
|
||||||
|
Please enter your details.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
contentContainerStyle={{ justifyContent: "center" }}
|
||||||
|
keyboardVerticalOffset={50}
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
value={email}
|
keyboardType={"email-address"}
|
||||||
|
returnKeyType={"next"}
|
||||||
|
textContentType={"emailAddress"}
|
||||||
|
defaultValue={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
|
autoComplete={"email"}
|
||||||
|
autoCorrect={false}
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
// Move focus to the description field
|
||||||
|
passwordRef.current?.focus();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
ref={passwordRef}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
textContentType={"oneTimeCode"}
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
<Button
|
</KeyboardAvoidingView>
|
||||||
label="Login"
|
|
||||||
onPress={handleSignIn}
|
|
||||||
style={{marginBottom: 20}}
|
|
||||||
backgroundColor="#fd1775"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
label="Login with a QR Code"
|
|
||||||
onPress={() => {
|
|
||||||
getCameraPermissions(() => setShowCameraDialog(true));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style={{marginBottom: 20}}
|
|
||||||
backgroundColor="#fd1775"
|
|
||||||
/>
|
|
||||||
{isError && <Text center style={{marginBottom: 20}}>{`${error?.toString()?.split("]")?.[1]}`}</Text>}
|
|
||||||
|
|
||||||
<View row centerH marginB-5 gap-5>
|
<View flexG />
|
||||||
<Text text70>
|
|
||||||
Don't have an account?
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
onPress={() => setTab("register")}
|
|
||||||
label="Sign Up"
|
|
||||||
link
|
|
||||||
size={ButtonSize.xSmall}
|
|
||||||
padding-0
|
|
||||||
margin-0
|
|
||||||
text70
|
|
||||||
left
|
|
||||||
color="#fd1775"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View row centerH marginB-5 gap-5>
|
|
||||||
<Text text70>
|
|
||||||
Forgot your password?
|
|
||||||
</Text>
|
|
||||||
<Button
|
<Button
|
||||||
onPress={() => setTab("reset-password")}
|
label="Log in"
|
||||||
label="Reset password"
|
marginT-50
|
||||||
link
|
labelStyle={{
|
||||||
size={ButtonSize.xSmall}
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
padding-0
|
fontSize: 16,
|
||||||
margin-0
|
|
||||||
text70
|
|
||||||
left
|
|
||||||
color="#fd1775"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Camera Dialog */}
|
|
||||||
<Dialog
|
|
||||||
visible={showCameraDialog}
|
|
||||||
onDismiss={() => setShowCameraDialog(false)}
|
|
||||||
bottom
|
|
||||||
width="100%"
|
|
||||||
height="70%"
|
|
||||||
containerStyle={{padding: 0}}
|
|
||||||
>
|
|
||||||
{hasPermission === null ? (
|
|
||||||
<Text>Requesting camera permissions...</Text>
|
|
||||||
) : !hasPermission ? (
|
|
||||||
<Text>No access to camera</Text>
|
|
||||||
) : (
|
|
||||||
<CameraView
|
|
||||||
style={{flex: 1}}
|
|
||||||
onBarcodeScanned={handleQrCodeScanned}
|
|
||||||
barcodeScannerSettings={{
|
|
||||||
barcodeTypes: ["qr"],
|
|
||||||
}}
|
}}
|
||||||
|
onPress={handleSignIn}
|
||||||
|
style={{ marginBottom: 20, height: 50 }}
|
||||||
|
backgroundColor="#fd1775"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<Text center style={{ marginBottom: 20 }}>{`${
|
||||||
|
error?.toString()?.split("]")?.[1]
|
||||||
|
}`}</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View row centerH marginB-5 gap-5>
|
||||||
|
<Text style={styles.jakartaLight}>Don't have an account?</Text>
|
||||||
|
<Button
|
||||||
|
onPress={() => router.replace("/(unauth)/sign_up")}
|
||||||
|
label="Sign Up"
|
||||||
|
labelStyle={[
|
||||||
|
styles.jakartaMedium,
|
||||||
|
{ textDecorationLine: "none", color: "#fd1575" },
|
||||||
|
]}
|
||||||
|
link
|
||||||
|
size={ButtonSize.xSmall}
|
||||||
|
padding-0
|
||||||
|
margin-0
|
||||||
|
text70
|
||||||
|
left
|
||||||
|
color="#fd1775"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/*<View row centerH marginB-5 gap-5>*/}
|
||||||
|
{/* <Text text70>Forgot your password?</Text>*/}
|
||||||
|
{/* <Button*/}
|
||||||
|
{/* onPress={() => router.replace("/(unauth)/sign_up")}*/}
|
||||||
|
{/* label="Reset password"*/}
|
||||||
|
{/* labelStyle={[*/}
|
||||||
|
{/* styles.jakartaMedium,*/}
|
||||||
|
{/* {textDecorationLine: "none", color: "#fd1575"},*/}
|
||||||
|
{/* ]}*/}
|
||||||
|
{/* link*/}
|
||||||
|
{/* size={ButtonSize.xSmall}*/}
|
||||||
|
{/* padding-0*/}
|
||||||
|
{/* margin-0*/}
|
||||||
|
{/* text70*/}
|
||||||
|
{/* left*/}
|
||||||
|
{/* avoidInnerPadding*/}
|
||||||
|
{/* color="#fd1775"*/}
|
||||||
|
{/* />*/}
|
||||||
|
{/*</View>*/}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<LoaderScreen
|
||||||
|
overlay
|
||||||
|
message={"Signing in..."}
|
||||||
|
backgroundColor={Colors.white}
|
||||||
|
color={Colors.grey40}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
label="Cancel"
|
|
||||||
onPress={() => setShowCameraDialog(false)}
|
|
||||||
backgroundColor="#fd1775"
|
|
||||||
style={{margin: 10}}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
</View>
|
</View>
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -164,6 +175,18 @@ const styles = StyleSheet.create({
|
|||||||
padding: 30,
|
padding: 30,
|
||||||
height: 45,
|
height: 45,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
|
},
|
||||||
|
jakartaLight: {
|
||||||
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#484848",
|
||||||
|
},
|
||||||
|
jakartaMedium: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontSize: 16,
|
||||||
|
color: "#919191",
|
||||||
|
textDecorationLine: "underline",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Colors,
|
||||||
|
KeyboardAwareScrollView,
|
||||||
|
LoaderScreen,
|
||||||
Text,
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
TextFieldRef,
|
TextFieldRef,
|
||||||
@ -10,17 +13,15 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import { useSignUp } from "@/hooks/firebase/useSignUp";
|
import { useSignUp } from "@/hooks/firebase/useSignUp";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import { KeyboardAvoidingView, Platform, StyleSheet } from "react-native";
|
||||||
import { StyleSheet } from "react-native";
|
|
||||||
import { AntDesign } from "@expo/vector-icons";
|
import { AntDesign } from "@expo/vector-icons";
|
||||||
|
import KeyboardManager from "react-native-keyboard-manager";
|
||||||
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
|
||||||
const SignUpPage = ({
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
||||||
setTab,
|
|
||||||
}: {
|
const SignUpPage = () => {
|
||||||
setTab: React.Dispatch<
|
|
||||||
React.SetStateAction<"register" | "login" | "reset-password">
|
|
||||||
>;
|
|
||||||
}) => {
|
|
||||||
const [email, setEmail] = useState<string>("");
|
const [email, setEmail] = useState<string>("");
|
||||||
const [firstName, setFirstName] = useState<string>("");
|
const [firstName, setFirstName] = useState<string>("");
|
||||||
const [lastName, setLastName] = useState<string>("");
|
const [lastName, setLastName] = useState<string>("");
|
||||||
@ -29,31 +30,55 @@ const SignUpPage = ({
|
|||||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||||
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
const [allowFaceID, setAllowFaceID] = useState<boolean>(false);
|
||||||
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
const [acceptTerms, setAcceptTerms] = useState<boolean>(false);
|
||||||
const { mutateAsync: signUp } = useSignUp();
|
const { mutateAsync: signUp, isLoading } = useSignUp();
|
||||||
|
|
||||||
const lnameRef = useRef<TextFieldRef>(null);
|
const lnameRef = useRef<TextFieldRef>(null);
|
||||||
const emailRef = useRef<TextFieldRef>(null);
|
const emailRef = useRef<TextFieldRef>(null);
|
||||||
const passwordRef = useRef<TextFieldRef>(null);
|
const passwordRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
await signUp({ email, password, firstName, lastName });
|
await signUp({ email, password, firstName, lastName });
|
||||||
|
router.replace("/(unauth)/cal_sync");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View padding-10 height={"100%"} flexG>
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
<Text text30 center>
|
<KeyboardAwareScrollView
|
||||||
|
contentContainerStyle={{ flexGrow: 1 }}
|
||||||
|
enableOnAndroid
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{ flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%" }}
|
||||||
|
>
|
||||||
|
<View gap-13 width={"100%"} marginB-20>
|
||||||
|
<Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
|
||||||
Get started with Cally
|
Get started with Cally
|
||||||
</Text>
|
</Text>
|
||||||
<Text center>Please enter your details.</Text>
|
<Text color={"#919191"} style={{ fontSize: 20 }}>
|
||||||
|
Please enter your details.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView style={{ width: "100%" }}>
|
||||||
<TextField
|
<TextField
|
||||||
marginT-60
|
marginT-30
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="First name"
|
placeholder="First name"
|
||||||
value={firstName}
|
value={firstName}
|
||||||
onChangeText={setFirstName}
|
onChangeText={setFirstName}
|
||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
onSubmitEditing={() => {lnameRef.current?.focus()}}
|
onSubmitEditing={() => {
|
||||||
|
lnameRef.current?.focus();
|
||||||
|
}}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
|
accessibilityLabel="First name input"
|
||||||
|
accessibilityHint="Enter your first name"
|
||||||
|
accessible
|
||||||
|
returnKeyType="next"
|
||||||
|
textContentType="givenName"
|
||||||
|
importantForAccessibility="yes"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
ref={lnameRef}
|
ref={lnameRef}
|
||||||
@ -61,25 +86,44 @@ const SignUpPage = ({
|
|||||||
value={lastName}
|
value={lastName}
|
||||||
onChangeText={setLastName}
|
onChangeText={setLastName}
|
||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
onSubmitEditing={() => {emailRef.current?.focus()}}
|
onSubmitEditing={() => {
|
||||||
|
emailRef.current?.focus();
|
||||||
|
}}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
|
accessibilityLabel="Last name input"
|
||||||
|
accessibilityHint="Enter your last name"
|
||||||
|
accessible
|
||||||
|
returnKeyType="next"
|
||||||
|
textContentType="familyName"
|
||||||
|
importantForAccessibility="yes"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
ref={emailRef}
|
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
value={email}
|
keyboardType={"email-address"}
|
||||||
|
returnKeyType={"next"}
|
||||||
|
textContentType={"emailAddress"}
|
||||||
|
defaultValue={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
style={styles.textfield}
|
style={styles.textfield}
|
||||||
onSubmitEditing={() => {passwordRef.current?.focus()}}
|
autoComplete={"email"}
|
||||||
blurOnSubmit={false}
|
autoCorrect={false}
|
||||||
|
ref={emailRef}
|
||||||
|
onSubmitEditing={() => {
|
||||||
|
passwordRef.current?.focus();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
centerV
|
||||||
|
style={[styles.textfield, { padding: 0, paddingHorizontal: 30 }]}
|
||||||
|
>
|
||||||
<TextField
|
<TextField
|
||||||
ref={passwordRef}
|
ref={passwordRef}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
style={styles.jakartaLight}
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
secureTextEntry={!isPasswordVisible}
|
secureTextEntry={!isPasswordVisible}
|
||||||
style={styles.textfield}
|
|
||||||
trailingAccessory={
|
trailingAccessory={
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
onPress={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||||
@ -92,36 +136,41 @@ const SignUpPage = ({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<View gap-10 marginH-10>
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
<View gap-5 marginT-15>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
style={[styles.check]}
|
||||||
|
color="#919191"
|
||||||
value={allowFaceID}
|
value={allowFaceID}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setAllowFaceID(value);
|
setAllowFaceID(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text text90R marginL-10>
|
<Text style={styles.jakartaLight} marginL-10>
|
||||||
Allow FaceID for login in future
|
Allow FaceID for login in future
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
style={styles.check}
|
||||||
|
color="#919191"
|
||||||
value={acceptTerms}
|
value={acceptTerms}
|
||||||
onValueChange={(value) => setAcceptTerms(value)}
|
onValueChange={(value) => setAcceptTerms(value)}
|
||||||
/>
|
/>
|
||||||
<View row>
|
<View row style={{ flexWrap: "wrap", marginLeft: 10 }}>
|
||||||
<Text text90R marginL-10>
|
<Text style={styles.jakartaLight}>I accept the</Text>
|
||||||
I accept the
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity>
|
<TouchableOpacity>
|
||||||
<Text text90 style={{ textDecorationLine: "underline" }}>
|
<Text text90 style={styles.jakartaMedium}>
|
||||||
{" "}
|
{" "}
|
||||||
terms and conditions
|
terms and conditions
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text text90R> and </Text>
|
<Text style={styles.jakartaLight}> and </Text>
|
||||||
<TouchableOpacity>
|
<TouchableOpacity>
|
||||||
<Text text90 style={{ textDecorationLine: "underline" }}>
|
<Text text90 style={styles.jakartaMedium}>
|
||||||
{" "}
|
{" "}
|
||||||
privacy policy
|
privacy policy
|
||||||
</Text>
|
</Text>
|
||||||
@ -129,30 +178,64 @@ const SignUpPage = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.bottomView}>
|
|
||||||
|
<View flexG style={{ minHeight: 50 }} />
|
||||||
|
|
||||||
|
<View>
|
||||||
<Button
|
<Button
|
||||||
label="Register"
|
label="Register"
|
||||||
|
disabled={!acceptTerms}
|
||||||
|
labelStyle={{
|
||||||
|
fontFamily: "PlusJakartaSans_600SemiBold",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
onPress={handleSignUp}
|
onPress={handleSignUp}
|
||||||
style={{ marginBottom: 10, backgroundColor: "#fd1775" }}
|
backgroundColor={"#fd1775"}
|
||||||
|
style={{ marginBottom: 0, height: 50 }}
|
||||||
/>
|
/>
|
||||||
<View row centerH marginT-10 marginB-5 gap-5>
|
<View row centerH marginT-10 marginB-2 gap-5>
|
||||||
<Text text70 center>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.jakartaLight,
|
||||||
|
{ fontSize: 16, color: "#484848" },
|
||||||
|
]}
|
||||||
|
center
|
||||||
|
>
|
||||||
Already have an account?
|
Already have an account?
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
label="Sign In"
|
label="Log in"
|
||||||
|
labelStyle={[
|
||||||
|
styles.jakartaMedium,
|
||||||
|
{
|
||||||
|
fontSize: 16,
|
||||||
|
textDecorationLine: "none",
|
||||||
|
color: "#fd1775",
|
||||||
|
},
|
||||||
|
]}
|
||||||
flexS
|
flexS
|
||||||
margin-0
|
margin-0
|
||||||
link
|
link
|
||||||
color="#fd1775"
|
color="#fd1775"
|
||||||
size={ButtonSize.small}
|
size={ButtonSize.small}
|
||||||
text70
|
text70
|
||||||
onPress={() => setTab("login")}
|
onPress={() => router.replace("/(unauth)/sign_in")}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</KeyboardAwareScrollView>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<LoaderScreen
|
||||||
|
overlay
|
||||||
|
message={"Signing up..."}
|
||||||
|
backgroundColor={Colors.white}
|
||||||
|
color={Colors.grey40}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -161,11 +244,33 @@ export default SignUpPage;
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
textfield: {
|
textfield: {
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
marginVertical: 10,
|
marginVertical: 8,
|
||||||
padding: 30,
|
padding: 30,
|
||||||
height: 45,
|
height: 44,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#919191",
|
||||||
|
},
|
||||||
|
jakartaLight: {
|
||||||
|
fontFamily: "PlusJakartaSans_300Light",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#919191",
|
||||||
|
},
|
||||||
|
jakartaMedium: {
|
||||||
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#919191",
|
||||||
|
textDecorationLine: "underline",
|
||||||
|
},
|
||||||
|
title: { fontFamily: "Manrope_600SemiBold", fontSize: 34, marginTop: 50 },
|
||||||
|
subtitle: { fontFamily: "PlusJakartaSans_400Regular", fontSize: 16 },
|
||||||
|
check: {
|
||||||
|
borderRadius: 3,
|
||||||
|
aspectRatio: 1,
|
||||||
|
width: 18,
|
||||||
|
color: "#919191",
|
||||||
|
borderColor: "#919191",
|
||||||
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
//mora da se izmeni kako treba
|
|
||||||
bottomView: { marginTop: 150 },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
import {AntDesign, Ionicons} from "@expo/vector-icons";
|
||||||
import React, {useCallback, useEffect, useState} from "react";
|
import React, {useCallback, useState} from "react";
|
||||||
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
|
import {Button, Checkbox, Text, View} from "react-native-ui-lib";
|
||||||
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
|
import {ActivityIndicator, ScrollView, StyleSheet} from "react-native";
|
||||||
import {TouchableOpacity} from "react-native-gesture-handler";
|
import {TouchableOpacity} from "react-native-gesture-handler";
|
||||||
@ -9,51 +9,49 @@ import debounce from "debounce";
|
|||||||
import AppleIcon from "@/assets/svgs/AppleIcon";
|
import AppleIcon from "@/assets/svgs/AppleIcon";
|
||||||
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
import GoogleIcon from "@/assets/svgs/GoogleIcon";
|
||||||
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
import OutlookIcon from "@/assets/svgs/OutlookIcon";
|
||||||
import * as AuthSession from "expo-auth-session";
|
|
||||||
import * as Google from "expo-auth-session/providers/google";
|
|
||||||
import * as WebBrowser from "expo-web-browser";
|
|
||||||
import {UserProfile} from "@firebase/auth";
|
|
||||||
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
|
|
||||||
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
|
|
||||||
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
|
|
||||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
|
||||||
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
|
import ExpoLocalization from "expo-localization/src/ExpoLocalization";
|
||||||
import {colorMap} from "@/constants/colorMap";
|
import {colorMap} from "@/constants/colorMap";
|
||||||
|
import {useSetAtom} from "jotai";
|
||||||
|
import {settingsPageIndex} from "../calendar/atoms";
|
||||||
|
import CalendarSettingsDialog from "./calendar_components/CalendarSettingsDialog";
|
||||||
|
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
|
||||||
|
import {useCalSync} from "@/hooks/useCalSync";
|
||||||
|
import Feather from "@expo/vector-icons/Feather";
|
||||||
|
|
||||||
const googleConfig = {
|
|
||||||
androidClientId:
|
|
||||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
|
||||||
iosClientId:
|
|
||||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
|
||||||
webClientId:
|
|
||||||
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
|
||||||
scopes: [
|
|
||||||
"email",
|
|
||||||
"profile",
|
|
||||||
"https://www.googleapis.com/auth/calendar.events.owned",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const microsoftConfig = {
|
const CalendarSettingsPage = () => {
|
||||||
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
|
||||||
redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
|
|
||||||
scopes: [
|
|
||||||
"openid",
|
|
||||||
"profile",
|
|
||||||
"email",
|
|
||||||
"offline_access",
|
|
||||||
"Calendars.ReadWrite",
|
|
||||||
"User.Read"
|
|
||||||
],
|
|
||||||
authorizationEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
||||||
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
||||||
};
|
|
||||||
|
|
||||||
const CalendarSettingsPage = (props: {
|
|
||||||
setSelectedPage: (page: number) => void;
|
|
||||||
}) => {
|
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(profileData?.firstDayOfWeek ?? ExpoLocalization.getCalendars()[0].firstWeekday === 1 ? "Mondays" : "Sundays");
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
|
|
||||||
|
const [firstDayOfWeek, setFirstDayOfWeek] = useState<string>(
|
||||||
|
profileData?.firstDayOfWeek ??
|
||||||
|
ExpoLocalization.getCalendars()[0].firstWeekday === 1
|
||||||
|
? "Mondays"
|
||||||
|
: "Sundays"
|
||||||
|
);
|
||||||
|
const [isModalVisible, setModalVisible] = useState<boolean>(false);
|
||||||
|
const [selectedService, setSelectedService] = useState<
|
||||||
|
"google" | "outlook" | "apple"
|
||||||
|
>("google");
|
||||||
|
const [selectedEmail, setSelectedEmail] = useState<string>("");
|
||||||
|
|
||||||
|
const showConfirmationDialog = (
|
||||||
|
serviceName: "google" | "outlook" | "apple",
|
||||||
|
email: string
|
||||||
|
) => {
|
||||||
|
setSelectedService(serviceName);
|
||||||
|
setSelectedEmail(email);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
await clearToken({email: selectedEmail, provider: selectedService});
|
||||||
|
setModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
const [selectedColor, setSelectedColor] = useState<string>(
|
const [selectedColor, setSelectedColor] = useState<string>(
|
||||||
profileData?.eventColor ?? colorMap.pink
|
profileData?.eventColor ?? colorMap.pink
|
||||||
@ -63,169 +61,22 @@ const CalendarSettingsPage = (props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {mutateAsync: updateUserData} = useUpdateUserData();
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} = useFetchAndSaveGoogleEvents();
|
const {mutateAsync: clearToken} = useClearTokens();
|
||||||
const {mutateAsync: fetchAndSaveOutlookEvents, isLoading: isSyncingOutlook} = useFetchAndSaveOutlookEvents();
|
|
||||||
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} = useFetchAndSaveAppleEvents();
|
|
||||||
|
|
||||||
|
const {
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
isSyncingGoogle,
|
||||||
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
isSyncingOutlook,
|
||||||
|
isConnectedToGoogle,
|
||||||
useEffect(() => {
|
isConnectedToMicrosoft,
|
||||||
signInWithGoogle();
|
isConnectedToApple,
|
||||||
}, [response]);
|
handleAppleSignIn,
|
||||||
|
isSyncingApple,
|
||||||
const signInWithGoogle = async () => {
|
handleMicrosoftSignIn,
|
||||||
try {
|
fetchAndSaveOutlookEvents,
|
||||||
if (response?.type === "success") {
|
fetchAndSaveGoogleEvents,
|
||||||
const accessToken = response.authentication?.accessToken;
|
handleStartGoogleSignIn,
|
||||||
|
fetchAndSaveAppleEvents
|
||||||
const userInfoResponse = await fetch(
|
} = useCalSync()
|
||||||
"https://www.googleapis.com/oauth2/v3/userinfo",
|
|
||||||
{
|
|
||||||
headers: {Authorization: `Bearer ${accessToken}`},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const userInfo = await userInfoResponse.json();
|
|
||||||
const googleMail = userInfo.email;
|
|
||||||
|
|
||||||
let googleAccounts = profileData?.googleAccounts;
|
|
||||||
const updatedGoogleAccounts = googleAccounts ? {...googleAccounts, [googleMail]: accessToken} : {[googleMail]: accessToken};
|
|
||||||
|
|
||||||
await updateUserData({
|
|
||||||
newUserData: {googleAccounts: updatedGoogleAccounts},
|
|
||||||
});
|
|
||||||
|
|
||||||
await fetchAndSaveGoogleEvents({token: accessToken, email: googleMail})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during Google sign-in:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMicrosoftSignIn = async () => {
|
|
||||||
try {
|
|
||||||
console.log("Starting Microsoft sign-in...");
|
|
||||||
|
|
||||||
const authRequest = new AuthSession.AuthRequest({
|
|
||||||
clientId: microsoftConfig.clientId,
|
|
||||||
scopes: microsoftConfig.scopes,
|
|
||||||
redirectUri: microsoftConfig.redirectUri,
|
|
||||||
responseType: AuthSession.ResponseType.Code,
|
|
||||||
usePKCE: true, // Enable PKCE
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Auth request created:", authRequest);
|
|
||||||
|
|
||||||
const authResult = await authRequest.promptAsync({
|
|
||||||
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Auth result:", authResult);
|
|
||||||
|
|
||||||
if (authResult.type === "success" && authResult.params?.code) {
|
|
||||||
const code = authResult.params.code;
|
|
||||||
console.log("Authorization code received:", code);
|
|
||||||
|
|
||||||
// Exchange authorization code for tokens
|
|
||||||
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: `client_id=${
|
|
||||||
microsoftConfig.clientId
|
|
||||||
}&redirect_uri=${encodeURIComponent(
|
|
||||||
microsoftConfig.redirectUri
|
|
||||||
)}&grant_type=authorization_code&code=${code}&code_verifier=${
|
|
||||||
authRequest.codeVerifier
|
|
||||||
}&scope=${encodeURIComponent(
|
|
||||||
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Token response status:", tokenResponse.status);
|
|
||||||
|
|
||||||
if (!tokenResponse.ok) {
|
|
||||||
const errorText = await tokenResponse.text();
|
|
||||||
console.error("Token exchange failed:", errorText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await tokenResponse.json();
|
|
||||||
console.log("Token data received:", tokenData);
|
|
||||||
|
|
||||||
if (tokenData?.access_token) {
|
|
||||||
console.log("Access token received, fetching user info...");
|
|
||||||
|
|
||||||
// Fetch user info from Microsoft Graph API to get the email
|
|
||||||
const userInfoResponse = await fetch("https://graph.microsoft.com/v1.0/me", {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${tokenData.access_token}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const userInfo = await userInfoResponse.json();
|
|
||||||
console.log("User info received:", userInfo);
|
|
||||||
|
|
||||||
if (userInfo.error) {
|
|
||||||
console.error("Error fetching user info:", userInfo.error);
|
|
||||||
} else {
|
|
||||||
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
|
|
||||||
|
|
||||||
let microsoftAccounts = profileData?.microsoftAccounts;
|
|
||||||
const updatedMicrosoftAccounts = microsoftAccounts ? {...microsoftAccounts, [outlookMail]: tokenData.access_token} : {[outlookMail]: tokenData.access_token};
|
|
||||||
|
|
||||||
// Update user data with Microsoft token and email
|
|
||||||
await updateUserData({
|
|
||||||
newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
|
|
||||||
});
|
|
||||||
|
|
||||||
await fetchAndSaveOutlookEvents(tokenData.access_token, outlookMail)
|
|
||||||
console.log("User data updated successfully.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn("Authentication was not successful:", authResult);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during Microsoft sign-in:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAppleSignIn = async () => {
|
|
||||||
try {
|
|
||||||
console.log("Starting Apple Sign-in...");
|
|
||||||
|
|
||||||
const credential = await AppleAuthentication.signInAsync({
|
|
||||||
requestedScopes: [
|
|
||||||
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
|
||||||
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Apple sign-in result:", credential);
|
|
||||||
|
|
||||||
const appleToken = credential.identityToken;
|
|
||||||
const appleMail = credential.email;
|
|
||||||
|
|
||||||
if (appleToken) {
|
|
||||||
console.log("Apple ID token received. Fetch user info if needed...");
|
|
||||||
|
|
||||||
await updateUserData({
|
|
||||||
newUserData: {appleToken, appleMail},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("User data updated with Apple ID token.");
|
|
||||||
await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
|
|
||||||
} else {
|
|
||||||
console.warn("Apple authentication was not successful or email was hidden.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during Apple Sign-in:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedUpdateUserData = useCallback(
|
const debouncedUpdateUserData = useCallback(
|
||||||
debounce(async (color: string) => {
|
debounce(async (color: string) => {
|
||||||
@ -259,9 +110,9 @@ const CalendarSettingsPage = (props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
|
const handleChangeFirstDayOfWeek = (firstDayOfWeek: string) => {
|
||||||
setFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
|
setFirstDayOfWeek(firstDayOfWeek);
|
||||||
debouncedUpdateFirstDayOfWeek(firstDayOfWeek === "Sundays" ? "Mondays" : "Sundays");
|
debouncedUpdateFirstDayOfWeek(firstDayOfWeek);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleChangeColor = (color: string) => {
|
const handleChangeColor = (color: string) => {
|
||||||
setPreviousSelectedColor(selectedColor);
|
setPreviousSelectedColor(selectedColor);
|
||||||
@ -269,66 +120,10 @@ const CalendarSettingsPage = (props: {
|
|||||||
debouncedUpdateUserData(color);
|
debouncedUpdateUserData(color);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearToken = async (provider: "google" | "outlook" | "apple", email: string) => {
|
|
||||||
const newUserData: Partial<UserProfile> = {};
|
|
||||||
if (provider === "google") {
|
|
||||||
let googleAccounts = profileData?.googleAccounts;
|
|
||||||
if (googleAccounts) {
|
|
||||||
googleAccounts[email] = null;
|
|
||||||
newUserData.googleAccounts = googleAccounts;
|
|
||||||
}
|
|
||||||
} else if (provider === "outlook") {
|
|
||||||
let microsoftAccounts = profileData?.microsoftAccounts;
|
|
||||||
if (microsoftAccounts) {
|
|
||||||
microsoftAccounts[email] = null;
|
|
||||||
newUserData.microsoftAccounts = microsoftAccounts;
|
|
||||||
}
|
|
||||||
} else if (provider === "apple") {
|
|
||||||
let appleAccounts = profileData?.appleAccounts;
|
|
||||||
if (appleAccounts) {
|
|
||||||
appleAccounts[email] = null;
|
|
||||||
newUserData.appleAccounts = appleAccounts;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await updateUserData({newUserData});
|
|
||||||
};
|
|
||||||
|
|
||||||
let isConnectedToGoogle = false;
|
|
||||||
if (profileData?.googleAccounts) {
|
|
||||||
Object.values(profileData?.googleAccounts).forEach((item) => {
|
|
||||||
if (item !== null) {
|
|
||||||
isConnectedToGoogle = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let isConnectedToMicrosoft = false;
|
|
||||||
const microsoftAccounts = profileData?.microsoftAccounts;
|
|
||||||
if (microsoftAccounts) {
|
|
||||||
Object.values(profileData?.microsoftAccounts).forEach((item) => {
|
|
||||||
if (item !== null) {
|
|
||||||
isConnectedToMicrosoft = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let isConnectedToApple = false;
|
|
||||||
if (profileData?.appleAccounts) {
|
|
||||||
Object.values(profileData?.appleAccounts).forEach((item) => {
|
|
||||||
if (item !== null) {
|
|
||||||
isConnectedToApple = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<View marginH-30 marginB-30>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
|
<View row marginT-20 marginB-20 marginL-20 centerV>
|
||||||
<View row marginT-20 marginB-35 centerV>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-back"
|
name="chevron-back"
|
||||||
size={14}
|
size={14}
|
||||||
@ -343,6 +138,7 @@ const CalendarSettingsPage = (props: {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<View marginH-30 marginB-30>
|
||||||
<Text style={styles.subTitle}>Calendar settings</Text>
|
<Text style={styles.subTitle}>Calendar settings</Text>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle} marginB-14>
|
<Text style={styles.cardTitle} marginB-14>
|
||||||
@ -424,11 +220,11 @@ const CalendarSettingsPage = (props: {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={() => promptAsync()}
|
onPress={() => handleStartGoogleSignIn()}
|
||||||
label={"Connect Google"}
|
label={profileData?.googleAccounts ? "Connect another Google account" : "Connect Google account"}
|
||||||
labelStyle={styles.addCalLbl}
|
labelStyle={styles.addCalLbl}
|
||||||
labelProps={{
|
labelProps={{
|
||||||
numberOfLines: 2
|
numberOfLines: 2,
|
||||||
}}
|
}}
|
||||||
iconSource={() => (
|
iconSource={() => (
|
||||||
<View marginR-15>
|
<View marginR-15>
|
||||||
@ -439,33 +235,14 @@ const CalendarSettingsPage = (props: {
|
|||||||
color="black"
|
color="black"
|
||||||
text70BL
|
text70BL
|
||||||
/>
|
/>
|
||||||
{profileData?.googleAccounts ? Object.keys(profileData?.googleAccounts)?.map((googleMail) => {
|
|
||||||
const googleToken = profileData?.googleAccounts?.[googleMail];
|
|
||||||
return googleToken && <Button
|
|
||||||
key={googleMail}
|
|
||||||
onPress={() => clearToken("google", googleMail)}
|
|
||||||
label={`Disconnect ${googleMail}`}
|
|
||||||
labelStyle={styles.addCalLbl}
|
|
||||||
labelProps={{
|
|
||||||
numberOfLines: 2
|
|
||||||
}}
|
|
||||||
iconSource={() => (
|
|
||||||
<View marginR-15>
|
|
||||||
<GoogleIcon/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
style={styles.addCalBtn}
|
|
||||||
color="black"
|
|
||||||
text70BL
|
|
||||||
/>
|
|
||||||
}) : null}
|
|
||||||
|
|
||||||
|
{!profileData?.appleAccounts && (
|
||||||
<Button
|
<Button
|
||||||
onPress={() => handleAppleSignIn()}
|
onPress={() => handleAppleSignIn()}
|
||||||
label={"Connect Apple"}
|
label={"Connect Apple"}
|
||||||
labelStyle={styles.addCalLbl}
|
labelStyle={styles.addCalLbl}
|
||||||
labelProps={{
|
labelProps={{
|
||||||
numberOfLines: 2
|
numberOfLines: 2,
|
||||||
}}
|
}}
|
||||||
iconSource={() => (
|
iconSource={() => (
|
||||||
<View marginR-15>
|
<View marginR-15>
|
||||||
@ -476,33 +253,14 @@ const CalendarSettingsPage = (props: {
|
|||||||
color="black"
|
color="black"
|
||||||
text70BL
|
text70BL
|
||||||
/>
|
/>
|
||||||
{profileData?.appleAccounts ? Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
|
|
||||||
const appleToken = profileData?.appleAccounts?.[appleEmail];
|
|
||||||
return appleToken && <Button
|
|
||||||
key={appleEmail}
|
|
||||||
onPress={() => clearToken("apple", appleEmail)}
|
|
||||||
label={`Disconnect ${appleEmail}`}
|
|
||||||
labelStyle={styles.addCalLbl}
|
|
||||||
labelProps={{
|
|
||||||
numberOfLines: 2
|
|
||||||
}}
|
|
||||||
iconSource={() => (
|
|
||||||
<View marginR-15>
|
|
||||||
<AppleIcon/>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
style={styles.addCalBtn}
|
|
||||||
color="black"
|
|
||||||
text70BL
|
|
||||||
/>
|
|
||||||
}) : null}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={() => handleMicrosoftSignIn()}
|
onPress={() => handleMicrosoftSignIn()}
|
||||||
label={"Connect Outlook"}
|
label={profileData?.microsoftAccounts ? "Connect another Outlook account" : "Connect Outlook"}
|
||||||
labelStyle={styles.addCalLbl}
|
labelStyle={styles.addCalLbl}
|
||||||
labelProps={{
|
labelProps={{
|
||||||
numberOfLines: 2
|
numberOfLines: 2,
|
||||||
}}
|
}}
|
||||||
iconSource={() => (
|
iconSource={() => (
|
||||||
<View marginR-15>
|
<View marginR-15>
|
||||||
@ -513,147 +271,220 @@ const CalendarSettingsPage = (props: {
|
|||||||
color="black"
|
color="black"
|
||||||
text70BL
|
text70BL
|
||||||
/>
|
/>
|
||||||
{profileData?.microsoftAccounts ? Object.keys(profileData?.microsoftAccounts)?.map((microsoftEmail) => {
|
|
||||||
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
|
||||||
return microsoftToken && <Button
|
|
||||||
key={microsoftEmail}
|
|
||||||
onPress={() => clearToken("outlook", microsoftEmail)}
|
|
||||||
label={`Disconnect ${microsoftEmail}`}
|
|
||||||
labelStyle={styles.addCalLbl}
|
|
||||||
labelProps={{
|
|
||||||
numberOfLines: 2
|
|
||||||
}}
|
|
||||||
iconSource={() => (
|
|
||||||
<View marginR-15>
|
|
||||||
<OutlookIcon/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
style={styles.addCalBtn}
|
|
||||||
color="black"
|
|
||||||
text70BL
|
|
||||||
/>
|
|
||||||
}) : null}
|
|
||||||
|
|
||||||
{(isConnectedToGoogle || isConnectedToMicrosoft || isConnectedToApple) && (
|
{(isConnectedToGoogle ||
|
||||||
|
isConnectedToMicrosoft ||
|
||||||
|
isConnectedToApple) && (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.subTitle} marginT-30 marginB-20>
|
<Text style={styles.subTitle} marginT-30 marginB-20>
|
||||||
Connected Calendars
|
Connected Calendars
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.noPaddingCard}>
|
<View style={[styles.noPaddingCard, {marginBottom: 100}]}>
|
||||||
<View style={{marginTop: 20}}>
|
<View style={{marginTop: 20}}>
|
||||||
{profileData?.googleAccounts && Object.keys(profileData?.googleAccounts)?.map((googleEmail) => {
|
{profileData?.googleAccounts &&
|
||||||
const googleToken = profileData?.googleAccounts?.[googleEmail];
|
Object.keys(profileData?.googleAccounts)?.map(
|
||||||
return googleToken && (
|
(googleEmail) => {
|
||||||
<TouchableOpacity
|
const googleToken =
|
||||||
onPress={() => fetchAndSaveGoogleEvents({token: googleToken, email: googleEmail})}
|
profileData?.googleAccounts?.[googleEmail]?.accessToken;
|
||||||
|
return (
|
||||||
|
googleToken && (
|
||||||
|
<View row paddingR-5 center>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
marginBottom: 15,
|
||||||
|
paddingLeft: 15,
|
||||||
|
}}
|
||||||
|
color="black"
|
||||||
|
text70BL
|
||||||
|
row
|
||||||
|
centerV
|
||||||
|
width="100%"
|
||||||
|
spread
|
||||||
>
|
>
|
||||||
<View row paddingR-20 center>
|
{isSyncingGoogle ? (
|
||||||
|
<View marginR-5>
|
||||||
|
<ActivityIndicator/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View marginR-5>
|
||||||
<Button
|
<Button
|
||||||
disabled={isSyncingGoogle}
|
style={{backgroundColor: "#ffffff"}}
|
||||||
onPress={() => fetchAndSaveGoogleEvents({token: googleToken, email: googleEmail})}
|
color="black"
|
||||||
label={`Sync ${googleEmail}`}
|
onPress={() =>
|
||||||
labelStyle={styles.addCalLbl}
|
fetchAndSaveGoogleEvents({
|
||||||
labelProps={{numberOfLines: 3}}
|
token: googleToken,
|
||||||
iconSource={() => (
|
email: googleEmail,
|
||||||
<View marginR-15>
|
})
|
||||||
|
}
|
||||||
|
iconSource={() => <Ionicons
|
||||||
|
name={"refresh"}
|
||||||
|
size={20}
|
||||||
|
color={"#000000"}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View marginR-5>
|
||||||
<GoogleIcon/>
|
<GoogleIcon/>
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={styles.addCalLbl}>
|
||||||
|
{googleEmail}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
style={{backgroundColor: "#ffffff", marginRight: 5}}
|
||||||
|
color="black"
|
||||||
|
onPress={
|
||||||
|
() => showConfirmationDialog("google", googleEmail)
|
||||||
|
}
|
||||||
|
iconSource={() => <Feather name="x" size={24} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
style={styles.addCalBtn}
|
|
||||||
|
{profileData?.appleAccounts &&
|
||||||
|
Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
|
||||||
|
console.log(profileData?.appleAccounts)
|
||||||
|
|
||||||
|
const appleToken = profileData?.appleAccounts?.[appleEmail];
|
||||||
|
return (
|
||||||
|
appleToken && (
|
||||||
|
<View row paddingR-5 center>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
marginBottom: 15,
|
||||||
|
paddingLeft: 15,
|
||||||
|
}}
|
||||||
color="black"
|
color="black"
|
||||||
text70BL
|
text70BL
|
||||||
/>
|
row
|
||||||
|
centerV
|
||||||
{isSyncingGoogle ? (
|
width="100%"
|
||||||
<ActivityIndicator/>
|
spread
|
||||||
) : (
|
>
|
||||||
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
|
<View marginR-5>
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{profileData?.appleAccounts && Object.keys(profileData?.appleAccounts)?.map((appleEmail) => {
|
|
||||||
const appleToken = profileData?.appleAccounts?.[appleEmail];
|
|
||||||
return appleToken && (
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => fetchAndSaveAppleEvents({
|
|
||||||
email: appleEmail,
|
|
||||||
token: appleToken
|
|
||||||
})}>
|
|
||||||
<View row paddingR-20 center>
|
|
||||||
<Button
|
|
||||||
disabled={isSyncingApple}
|
|
||||||
onPress={() => fetchAndSaveAppleEvents({
|
|
||||||
email: appleEmail,
|
|
||||||
token: appleToken
|
|
||||||
})}
|
|
||||||
label={`Sync ${appleEmail}`}
|
|
||||||
labelStyle={styles.addCalLbl}
|
|
||||||
labelProps={{numberOfLines: 3}}
|
|
||||||
iconSource={() => (
|
|
||||||
<View marginR-15>
|
|
||||||
<AppleIcon/>
|
<AppleIcon/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
<Text style={styles.addCalLbl}>
|
||||||
style={styles.addCalBtn}
|
{appleEmail}
|
||||||
color="black"
|
</Text>
|
||||||
text70BL
|
|
||||||
/>
|
|
||||||
{isSyncingApple ? (
|
{isSyncingApple ? (
|
||||||
|
<View marginR-5>
|
||||||
<ActivityIndicator/>
|
<ActivityIndicator/>
|
||||||
) : (
|
|
||||||
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
) : (
|
||||||
|
<View marginR-5>
|
||||||
|
<Button
|
||||||
|
style={{backgroundColor: "#ffffff"}}
|
||||||
|
color="black"
|
||||||
|
onPress={() =>
|
||||||
|
fetchAndSaveAppleEvents({
|
||||||
|
email: appleEmail,
|
||||||
|
token: appleToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
iconSource={() => <Ionicons
|
||||||
|
name={"refresh"}
|
||||||
|
size={20}
|
||||||
|
color={"#000000"}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
style={{backgroundColor: "#ffffff", marginRight: 5}}
|
||||||
|
color="black"
|
||||||
|
onPress={() => showConfirmationDialog("apple", appleEmail)}
|
||||||
|
iconSource={() => <Feather name="x" size={24} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{profileData?.microsoftAccounts && Object.keys(profileData?.microsoftAccounts)?.map((microsoftEmail) => {
|
{profileData?.microsoftAccounts &&
|
||||||
const microsoftToken = profileData?.microsoftAccounts?.[microsoftEmail];
|
Object.keys(profileData?.microsoftAccounts)?.map(
|
||||||
return microsoftToken && (
|
(microsoftEmail) => {
|
||||||
<TouchableOpacity
|
const microsoftToken =
|
||||||
onPress={() => fetchAndSaveOutlookEvents({
|
profileData?.microsoftAccounts?.[microsoftEmail];
|
||||||
token: microsoftToken,
|
return (
|
||||||
email: microsoftEmail
|
microsoftToken && (
|
||||||
})}
|
<View row paddingR-5 center>
|
||||||
>
|
<View
|
||||||
<View row paddingR-20 center>
|
style={{
|
||||||
<Button
|
backgroundColor: "#ffffff",
|
||||||
disabled={isSyncingOutlook}
|
marginBottom: 15,
|
||||||
onPress={() => fetchAndSaveOutlookEvents({
|
paddingLeft: 15,
|
||||||
token: microsoftToken,
|
}}
|
||||||
email: microsoftEmail
|
|
||||||
})}
|
|
||||||
label={`Sync ${microsoftEmail}`}
|
|
||||||
labelStyle={styles.addCalLbl}
|
|
||||||
labelProps={{numberOfLines: 3}}
|
|
||||||
iconSource={() => (
|
|
||||||
<View marginR-15>
|
|
||||||
<OutlookIcon/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
style={styles.addCalBtn}
|
|
||||||
color="black"
|
color="black"
|
||||||
text70BL
|
text70BL
|
||||||
/>
|
row
|
||||||
|
centerV
|
||||||
|
width="100%"
|
||||||
|
spread
|
||||||
|
>
|
||||||
{isSyncingOutlook ? (
|
{isSyncingOutlook ? (
|
||||||
|
<View marginR-5>
|
||||||
<ActivityIndicator/>
|
<ActivityIndicator/>
|
||||||
) : (
|
|
||||||
<Ionicons name={"refresh"} size={20} color={"#000000"}/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
) : (
|
||||||
|
<View marginR-5>
|
||||||
|
<Button
|
||||||
|
style={{backgroundColor: "#ffffff"}}
|
||||||
|
color="black"
|
||||||
|
onPress={() =>
|
||||||
|
fetchAndSaveOutlookEvents({
|
||||||
|
token: microsoftToken,
|
||||||
|
email: microsoftEmail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
iconSource={() => <Ionicons
|
||||||
|
name={"refresh"}
|
||||||
|
size={20}
|
||||||
|
color={"#000000"}
|
||||||
|
/>}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View marginR-5>
|
||||||
|
<OutlookIcon/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.addCalLbl}>
|
||||||
|
{microsoftEmail}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
style={{backgroundColor: "#ffffff", marginRight: 5}}
|
||||||
|
color="black"
|
||||||
|
onPress={
|
||||||
|
() => showConfirmationDialog("outlook", microsoftEmail)
|
||||||
|
}
|
||||||
|
iconSource={() => <Feather name="x" size={24} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
})}
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<CalendarSettingsDialog
|
||||||
|
visible={isModalVisible}
|
||||||
|
serviceName={selectedService}
|
||||||
|
email={selectedEmail}
|
||||||
|
onDismiss={handleCancel}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -698,10 +529,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontFamily: "PlusJakartaSan_500Medium",
|
fontFamily: "PlusJakartaSan_500Medium",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
width: "75%",
|
width: "70%",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
overflow: "visible"
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
subTitle: {
|
subTitle: {
|
||||||
fontFamily: "Manrope_600SemiBold",
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import { Ionicons } from "@expo/vector-icons";
|
|||||||
import { ToDosContextProvider } from "@/contexts/ToDosContext";
|
import { ToDosContextProvider } from "@/contexts/ToDosContext";
|
||||||
import ToDosList from "../todos/ToDosList";
|
import ToDosList from "../todos/ToDosList";
|
||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
|
import { settingsPageIndex } from "../calendar/atoms";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
|
const ChoreRewardSettings = () => {
|
||||||
|
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
||||||
|
|
||||||
const ChoreRewardSettings = (props: {
|
|
||||||
setSelectedPage: (page: number) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<ToDosContextProvider>
|
<ToDosContextProvider>
|
||||||
<View marginT-10 marginH-20>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<ScrollView>
|
<View row marginT-20 marginB-20 marginL-20 centerV>
|
||||||
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
|
|
||||||
<View row marginT-20 marginB-35 centerV>
|
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="chevron-back"
|
name="chevron-back"
|
||||||
size={14}
|
size={14}
|
||||||
@ -28,6 +28,8 @@ const ChoreRewardSettings = (props: {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
<View marginH-20>
|
||||||
|
<ScrollView>
|
||||||
<Text text60R marginB-20>
|
<Text text60R marginB-20>
|
||||||
Chore Reward Settings
|
Chore Reward Settings
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import {Button, Text, View} from "react-native-ui-lib";
|
import { Button, Text, View } from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {StyleSheet} from "react-native";
|
import {Linking, StyleSheet} from "react-native";
|
||||||
import {Octicons} from "@expo/vector-icons";
|
import { Octicons } from "@expo/vector-icons";
|
||||||
import CalendarSettingsPage from "./CalendarSettingsPage";
|
import CalendarSettingsPage from "./CalendarSettingsPage";
|
||||||
import ChoreRewardSettings from "./ChoreRewardSettings";
|
import ChoreRewardSettings from "./ChoreRewardSettings";
|
||||||
import UserSettings from "./UserSettings";
|
import UserSettings from "./UserSettings";
|
||||||
@ -9,7 +9,9 @@ import ProfileIcon from "@/assets/svgs/ProfileIcon";
|
|||||||
import CalendarIcon from "@/assets/svgs/CalendarIcon";
|
import CalendarIcon from "@/assets/svgs/CalendarIcon";
|
||||||
import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon";
|
import PrivacyPolicyIcon from "@/assets/svgs/PrivacyPolicyIcon";
|
||||||
import ArrowRightIcon from "@/assets/svgs/ArrowRightIcon";
|
import ArrowRightIcon from "@/assets/svgs/ArrowRightIcon";
|
||||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { settingsPageIndex } from "../calendar/atoms";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
|
||||||
const pageIndex = {
|
const pageIndex = {
|
||||||
main: 0,
|
main: 0,
|
||||||
@ -19,14 +21,26 @@ const pageIndex = {
|
|||||||
policy: 4,
|
policy: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const PRIVACY_POLICY_URL = 'https://callyapp.com';
|
||||||
const {profileData} = useAuthContext()
|
|
||||||
const isntParent = profileData?.userType !== ProfileType.PARENT
|
|
||||||
|
const SettingsPage = () => {
|
||||||
|
const { profileData } = useAuthContext();
|
||||||
|
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
|
||||||
|
const isntParent = profileData?.userType !== ProfileType.PARENT;
|
||||||
|
|
||||||
|
const openPrivacyPolicy = async () => {
|
||||||
|
const supported = await Linking.canOpenURL(PRIVACY_POLICY_URL);
|
||||||
|
if (supported) {
|
||||||
|
await Linking.openURL(PRIVACY_POLICY_URL);
|
||||||
|
} else {
|
||||||
|
console.log("Don't know how to open this URL:", PRIVACY_POLICY_URL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [selectedPage, setSelectedPage] = useState<number>(0);
|
|
||||||
return (
|
return (
|
||||||
<View flexG>
|
<View flexG>
|
||||||
{selectedPage == 0 && (
|
{pageIndex == 0 && (
|
||||||
<View flexG centerH marginH-30 marginT-30>
|
<View flexG centerH marginH-30 marginT-30>
|
||||||
<Button
|
<Button
|
||||||
disabled={isntParent}
|
disabled={isntParent}
|
||||||
@ -34,14 +48,19 @@ const SettingsPage = () => {
|
|||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
children={
|
children={
|
||||||
<View row centerV width={"100%"}>
|
<View row centerV width={"100%"}>
|
||||||
<ProfileIcon style={{marginRight: 10}} color="#07b9c8"/>
|
<ProfileIcon style={{ marginRight: 10 }} color="#07b9c8" />
|
||||||
<Text style={[styles.label, isntParent && styles.disabledText]}>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.label,
|
||||||
|
isntParent ? styles.disabledText : { color: "#07b9c8" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
Manage My Profile
|
Manage My Profile
|
||||||
</Text>
|
</Text>
|
||||||
<ArrowRightIcon style={{marginLeft: "auto"}}/>
|
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onPress={() => setSelectedPage(pageIndex.user)}
|
onPress={() => setPageIndex(1)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={isntParent}
|
disabled={isntParent}
|
||||||
@ -49,19 +68,25 @@ const SettingsPage = () => {
|
|||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
children={
|
children={
|
||||||
<View row centerV width={"100%"}>
|
<View row centerV width={"100%"}>
|
||||||
<CalendarIcon style={{marginRight: 10}}/>
|
<CalendarIcon style={{ marginRight: 10 }} />
|
||||||
<Text style={[styles.label, isntParent && styles.disabledText]}>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.label,
|
||||||
|
isntParent ? styles.disabledText : { color: "#FD1775" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
Calendar Settings
|
Calendar Settings
|
||||||
</Text>
|
</Text>
|
||||||
<ArrowRightIcon style={{marginLeft: "auto"}}/>
|
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedPage(pageIndex.calendar);
|
setPageIndex(2);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={isntParent}
|
disabled
|
||||||
|
// disabled={isntParent}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
children={
|
children={
|
||||||
@ -70,40 +95,33 @@ const SettingsPage = () => {
|
|||||||
name="gear"
|
name="gear"
|
||||||
size={24}
|
size={24}
|
||||||
color="#ff9900"
|
color="#ff9900"
|
||||||
style={{marginRight: 10}}
|
style={{ marginRight: 10 }}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.label, isntParent && styles.disabledText]}>
|
<Text style={[styles.label, true ? styles.disabledText : {color: "#ff9900"}]}>
|
||||||
To-Do Reward Settings
|
To-Do Reward Settings
|
||||||
</Text>
|
</Text>
|
||||||
<ArrowRightIcon style={{marginLeft: "auto"}}/>
|
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onPress={() => setSelectedPage(pageIndex.chore)}
|
onPress={() => setPageIndex(3)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
style={styles.mainBtn}
|
style={styles.mainBtn}
|
||||||
|
onPress={openPrivacyPolicy}
|
||||||
children={
|
children={
|
||||||
<View row centerV width={"100%"}>
|
<View row centerV width={"100%"}>
|
||||||
<PrivacyPolicyIcon style={{marginRight: 10}}/>
|
<PrivacyPolicyIcon style={{ marginRight: 10 }} />
|
||||||
<Text style={styles.label}>
|
<Text style={[styles.label]} color={"#6C645B"}>Cally Privacy Policy</Text>
|
||||||
Cally Privacy Policy
|
<ArrowRightIcon style={{ marginLeft: "auto" }} />
|
||||||
</Text>
|
|
||||||
<ArrowRightIcon style={{marginLeft: "auto"}}/>
|
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{selectedPage == pageIndex.calendar && (
|
{pageIndex == 2 && <CalendarSettingsPage />}
|
||||||
<CalendarSettingsPage setSelectedPage={setSelectedPage}/>
|
{pageIndex == 3 && <ChoreRewardSettings />}
|
||||||
)}
|
{pageIndex == 1 && <UserSettings />}
|
||||||
{selectedPage == pageIndex.chore && (
|
|
||||||
<ChoreRewardSettings setSelectedPage={setSelectedPage}/>
|
|
||||||
)}
|
|
||||||
{selectedPage == pageIndex.user && (
|
|
||||||
<UserSettings setSelectedPage={setSelectedPage}/>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -123,6 +141,6 @@ const styles = StyleSheet.create({
|
|||||||
textAlignVertical: "center",
|
textAlignVertical: "center",
|
||||||
},
|
},
|
||||||
disabledText: {
|
disabledText: {
|
||||||
color: '#A9A9A9', // Example of a gray color for disabled text
|
color: "#A9A9A9", // Example of a gray color for disabled text
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -1,77 +1,109 @@
|
|||||||
import { Text, TouchableOpacity, View } from "react-native-ui-lib";
|
import {FloatingButton, Text, TouchableOpacity, View,} from "react-native-ui-lib";
|
||||||
import React, { useState } from "react";
|
import React, {useState} from "react";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import {Ionicons} from "@expo/vector-icons";
|
||||||
import { ScrollView, StyleSheet } from "react-native";
|
import {ScrollView, StyleSheet} from "react-native";
|
||||||
import MyProfile from "./user_settings_views/MyProfile";
|
import MyProfile from "./user_settings_views/MyProfile";
|
||||||
import MyGroup from "./user_settings_views/MyGroup";
|
import MyGroup from "./user_settings_views/MyGroup";
|
||||||
|
import {useAtom, useSetAtom} from "jotai";
|
||||||
|
import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
|
||||||
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
|
const UserSettings = () => {
|
||||||
|
const setPageIndex = useSetAtom(settingsPageIndex);
|
||||||
|
const [userView, setUserView] = useAtom(userSettingsView);
|
||||||
|
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
|
||||||
|
|
||||||
const UserSettings = (props: { setSelectedPage: (page: number) => void }) => {
|
|
||||||
const [selectedView, setSelectedView] = useState<boolean>(true);
|
|
||||||
return (
|
return (
|
||||||
<View flexG>
|
<View flexG>
|
||||||
<ScrollView style={{ paddingBottom: 20, minHeight: "100%" }}>
|
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
|
||||||
<TouchableOpacity onPress={() => props.setSelectedPage(0)}>
|
<TouchableOpacity
|
||||||
<View row marginT-20 marginB-35 centerV>
|
onPress={() => {
|
||||||
<Ionicons name="chevron-back" size={14} color="#979797" style={{paddingBottom: 3}} />
|
setPageIndex(0);
|
||||||
|
setUserView(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View row marginT-20 marginB-20 marginL-20 centerV>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={14}
|
||||||
|
color="#979797"
|
||||||
|
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 main settings
|
Return to main settings
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View marginH-20 flexG style={{ minHeight: "90%" }}>
|
<View marginH-26 flexG style={{minHeight: "90%"}}>
|
||||||
<Text text60R marginB-25>
|
<Text text60R marginB-25>
|
||||||
User Management
|
User Management
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.buttonSwitch} spread row>
|
<View style={styles.buttonSwitch} spread row>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setSelectedView(true)}
|
onPress={() => setUserView(true)}
|
||||||
centerV
|
centerV
|
||||||
centerH
|
centerH
|
||||||
style={selectedView == true ? styles.btnSelected : styles.btnNot}
|
style={userView == true ? styles.btnSelected : styles.btnNot}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text
|
<Text
|
||||||
style={styles.btnTxt}
|
style={styles.btnTxt}
|
||||||
color={selectedView ? "white" : "black"}
|
color={userView ? "white" : "black"}
|
||||||
>
|
>
|
||||||
My Profile
|
My Profile
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setSelectedView(false)}
|
onPress={() => setUserView(false)}
|
||||||
centerV
|
centerV
|
||||||
centerH
|
centerH
|
||||||
style={selectedView == false ? styles.btnSelected : styles.btnNot}
|
style={userView == false ? styles.btnSelected : styles.btnNot}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text
|
<Text
|
||||||
style={styles.btnTxt}
|
style={styles.btnTxt}
|
||||||
color={!selectedView ? "white" : "black"}
|
color={!userView ? "white" : "black"}
|
||||||
>
|
>
|
||||||
My Group
|
My Group
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
{selectedView && <MyProfile />}
|
{userView && <MyProfile/>}
|
||||||
{!selectedView && <MyGroup />}
|
{!userView && <MyGroup onNewUserClick={onNewUserClick} setOnNewUserClick={setOnNewUserClick}/>}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
{!userView && (
|
||||||
{!selectedView && (
|
<FloatingButton
|
||||||
<View>
|
fullWidth
|
||||||
<Text>selview</Text>
|
hideBackgroundOverlay
|
||||||
</View>
|
visible
|
||||||
|
button={{
|
||||||
|
label: " Add a user device",
|
||||||
|
iconSource: () => <PlusIcon height={13} width={14}/>,
|
||||||
|
onPress: () => setOnNewUserClick(true),
|
||||||
|
style: styles.bottomButton,
|
||||||
|
labelStyle: {fontFamily: "Manrope_600SemiBold", fontSize: 15},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
bottomButton: {
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 15,
|
||||||
|
marginHorizontal: 28,
|
||||||
|
width: 337,
|
||||||
|
backgroundColor: "#e8156c",
|
||||||
|
height: 53.26,
|
||||||
|
},
|
||||||
buttonSwitch: {
|
buttonSwitch: {
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -93,7 +125,7 @@ const styles = StyleSheet.create({
|
|||||||
width: "50%",
|
width: "50%",
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
},
|
},
|
||||||
title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
|
title: {fontFamily: "Manrope_600SemiBold", fontSize: 18},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default UserSettings;
|
export default UserSettings;
|
||||||
|
|||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
interface ConfirmationDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
serviceName: "google" | "outlook" | "apple";
|
||||||
|
email: string;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarSettingsDialog: React.FC<ConfirmationDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
serviceName,
|
||||||
|
email,
|
||||||
|
onDismiss,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<Text center style={styles.title}>
|
||||||
|
Disconnect {serviceName}
|
||||||
|
</Text>
|
||||||
|
<View center>
|
||||||
|
<Text style={styles.text} center>
|
||||||
|
Are you sure you want to disconnect this {"\n"}
|
||||||
|
<Text style={{ fontSize: 16, fontFamily: "PlusJakartaSans_700Bold" }}>
|
||||||
|
{serviceName}
|
||||||
|
</Text>{" "}
|
||||||
|
calendar
|
||||||
|
{"\n"}
|
||||||
|
for {email}?
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View row right gap-8>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={onDismiss}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={onConfirm}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stylesheet for future styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
confirmBtn: {
|
||||||
|
backgroundColor: "#ea156d",
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
paddingTop: 35,
|
||||||
|
paddingBottom: 17,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 25,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default CalendarSettingsDialog;
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Dialog, Button, Text, View } from "react-native-ui-lib";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
|
||||||
|
interface ConfirmationDialogProps {
|
||||||
|
visible: boolean;
|
||||||
|
onDismiss: () => void;
|
||||||
|
onFirstYes: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
onDismiss,
|
||||||
|
onFirstYes,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const [confirmationDialog, setConfirmationDialog] = useState<boolean>(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
visible={visible}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<View centerH>
|
||||||
|
<Feather name="alert-triangle" size={70} color="#FF5449" />
|
||||||
|
</View>
|
||||||
|
<Text center style={styles.title}>
|
||||||
|
Are you sure?
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: "PlusJakartaSans_700Bold",
|
||||||
|
color: "#979797",
|
||||||
|
marginBottom: 20,
|
||||||
|
}}
|
||||||
|
center
|
||||||
|
>
|
||||||
|
This action will permanently delete all your data, you won't be able
|
||||||
|
to recover it!
|
||||||
|
</Text>
|
||||||
|
<View centerV></View>
|
||||||
|
<View row right gap-8>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={onDismiss}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={() => {
|
||||||
|
setTimeout(() => setConfirmationDialog(true), 300);
|
||||||
|
onFirstYes();
|
||||||
|
}}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog
|
||||||
|
visible={confirmationDialog}
|
||||||
|
onDismiss={() => setConfirmationDialog(false)}
|
||||||
|
containerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
<View center paddingH-10 paddingT-15 paddingB-5>
|
||||||
|
<Text style={styles.title}>
|
||||||
|
We're sorry to see you go, are you really sure you want to delete
|
||||||
|
everything?
|
||||||
|
</Text>
|
||||||
|
<View row right gap-8 marginT-15>
|
||||||
|
<Button
|
||||||
|
label="Cancel"
|
||||||
|
onPress={() => {
|
||||||
|
setConfirmationDialog(false);
|
||||||
|
}}
|
||||||
|
style={styles.cancelBtn}
|
||||||
|
color="#999999"
|
||||||
|
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label="Yes"
|
||||||
|
onPress={() => {
|
||||||
|
onConfirm();
|
||||||
|
setConfirmationDialog(false);
|
||||||
|
}}
|
||||||
|
style={styles.confirmBtn}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Empty stylesheet for future styles
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
confirmBtn: {
|
||||||
|
backgroundColor: "#FF5449",
|
||||||
|
},
|
||||||
|
cancelBtn: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
paddingHorizontal: 25,
|
||||||
|
paddingTop: 25,
|
||||||
|
paddingBottom: 17,
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontFamily: "PlusJakartaSans_400Regular",
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DeleteProfileDialogs;
|
||||||
@ -3,8 +3,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Colors,
|
Colors,
|
||||||
Dialog,
|
Dialog, Image,
|
||||||
FloatingButton,
|
|
||||||
KeyboardAwareScrollView,
|
KeyboardAwareScrollView,
|
||||||
PanningProvider,
|
PanningProvider,
|
||||||
Picker,
|
Picker,
|
||||||
@ -15,10 +14,10 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import React, {useEffect, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {ScrollView, StyleSheet} from "react-native";
|
import {ImageBackground, Platform, StyleSheet} from "react-native";
|
||||||
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
|
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
|
||||||
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
|
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
|
||||||
import {ProfileType} from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
|
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
@ -28,11 +27,18 @@ import CircledXIcon from "@/assets/svgs/CircledXIcon";
|
|||||||
import ProfileIcon from "@/assets/svgs/ProfileIcon";
|
import ProfileIcon from "@/assets/svgs/ProfileIcon";
|
||||||
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
|
||||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
import {PreviousNextView} from "react-native-keyboard-manager";
|
import KeyboardManager, {PreviousNextView,} from "react-native-keyboard-manager";
|
||||||
|
import {ScrollView} from "react-native-gesture-handler";
|
||||||
|
import {useUploadProfilePicture} from "@/hooks/useUploadProfilePicture";
|
||||||
|
import {ImagePickerAsset} from "expo-image-picker";
|
||||||
|
|
||||||
const MyGroup = () => {
|
type MyGroupProps = {
|
||||||
|
onNewUserClick: boolean;
|
||||||
|
setOnNewUserClick: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) => {
|
||||||
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
|
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
|
||||||
const [showNewUserInfoDialog, setShowNewUserInfoDialog] = useState(false);
|
|
||||||
const [selectedStatus, setSelectedStatus] = useState<
|
const [selectedStatus, setSelectedStatus] = useState<
|
||||||
string | PickerSingleValue
|
string | PickerSingleValue
|
||||||
>(ProfileType.CHILD);
|
>(ProfileType.CHILD);
|
||||||
@ -40,13 +46,19 @@ const MyGroup = () => {
|
|||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
|
||||||
|
const [newUserId, setNewUserId] = useState("")
|
||||||
|
|
||||||
const lNameRef = useRef<TextFieldRef>(null);
|
const lNameRef = useRef<TextFieldRef>(null);
|
||||||
const emailRef = useRef<TextFieldRef>(null);
|
const emailRef = useRef<TextFieldRef>(null);
|
||||||
|
|
||||||
const [showQRCodeDialog, setShowQRCodeDialog] = useState<string | boolean>(false);
|
const [showQRCodeDialog, setShowQRCodeDialog] = useState<string | boolean>(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
|
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
|
||||||
const {data: familyMembers} = useGetFamilyMembers(true);
|
const {data: familyMembers} = useGetFamilyMembers(true);
|
||||||
|
const {user} = useAuthContext();
|
||||||
|
const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
|
||||||
|
|
||||||
const parents =
|
const parents =
|
||||||
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
|
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
|
||||||
@ -67,10 +79,10 @@ const MyGroup = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedStatus !== ProfileType.FAMILY_DEVICE && !email) {
|
// if (selectedStatus !== ProfileType.FAMILY_DEVICE && !email) {
|
||||||
console.error("Email is required for non-family device users");
|
// console.error("Email is required for non-family device users");
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (email && !email.includes("@")) {
|
if (email && !email.includes("@")) {
|
||||||
console.error("Invalid email address");
|
console.error("Invalid email address");
|
||||||
@ -87,28 +99,38 @@ const MyGroup = () => {
|
|||||||
console.log(res);
|
console.log(res);
|
||||||
|
|
||||||
if (!isError) {
|
if (!isError) {
|
||||||
setShowNewUserInfoDialog(false);
|
setOnNewUserClick(false);
|
||||||
|
|
||||||
if (res?.data?.userId) {
|
if (res?.data?.userId) {
|
||||||
|
if (profileImageAsset) {
|
||||||
|
await changeProfilePicture(profileImageAsset)
|
||||||
|
setShowQRCodeDialog(res.data.userId);
|
||||||
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowQRCodeDialog(res.data.userId);
|
setShowQRCodeDialog(res.data.userId);
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClearImage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFirstName("");
|
setFirstName("");
|
||||||
setLastName("");
|
setLastName("");
|
||||||
setEmail("");
|
setEmail("");
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, minHeight: 500}}>
|
<View marginB-70>
|
||||||
|
<ScrollView>
|
||||||
<View>
|
<View>
|
||||||
<ScrollView style={styles.card}>
|
|
||||||
{!parents.length && !children.length && !caregivers.length && (
|
{!parents.length && !children.length && !caregivers.length && (
|
||||||
<Text text70 marginV-10>
|
<Text text70 marginV-10>
|
||||||
{isLoading ? "Loading...." : "No user devices added"}
|
{isLoading ? "Loading...." : "No user devices added"}
|
||||||
@ -116,8 +138,8 @@ const MyGroup = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{(!!parents.length || !!children.length) && (
|
{(!!parents.length || !!children.length) && (
|
||||||
<>
|
<View style={styles.card}>
|
||||||
<Text style={styles.subTit} marginV-10>
|
<Text style={styles.subTit} marginB-10>
|
||||||
Family
|
Family
|
||||||
</Text>
|
</Text>
|
||||||
{[...parents, ...children]?.map((member, index) => (
|
{[...parents, ...children]?.map((member, index) => (
|
||||||
@ -128,39 +150,45 @@ const MyGroup = () => {
|
|||||||
style={styles.familyCard}
|
style={styles.familyCard}
|
||||||
row
|
row
|
||||||
centerV
|
centerV
|
||||||
padding-10
|
paddingT-10
|
||||||
>
|
>
|
||||||
<Avatar
|
{member.pfp ? (
|
||||||
source={{uri: "https://via.placeholder.com/60"}}
|
<ImageBackground
|
||||||
size={40}
|
style={styles.pfp}
|
||||||
backgroundColor={Colors.grey60}
|
borderRadius={10.56}
|
||||||
|
source={{uri: member.pfp || undefined}}
|
||||||
/>
|
/>
|
||||||
<View marginL-10>
|
) : (
|
||||||
<Text text70M>
|
<View
|
||||||
|
style={[styles.pfp, {backgroundColor: "#ea156d"}]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<View row marginL-10 centerV>
|
||||||
|
<Text style={styles.name}>
|
||||||
{member.firstName} {member.lastName}
|
{member.firstName} {member.lastName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text text90 grey40>
|
</View>
|
||||||
|
<View flexG/>
|
||||||
|
<View row centerV gap-10>
|
||||||
|
<Text style={styles.userType}>
|
||||||
{member.userType === ProfileType.PARENT
|
{member.userType === ProfileType.PARENT
|
||||||
? "Admin (You)"
|
? `Admin${member.uid === user?.uid ? " (You)" : ""}`
|
||||||
: "Child"}
|
: "Child"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View flex-1/>
|
|
||||||
|
|
||||||
<UserMenu
|
<UserMenu
|
||||||
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
|
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
|
||||||
showQRCodeDialog={showQRCodeDialog === member?.uid}
|
showQRCodeDialog={showQRCodeDialog === member?.uid}
|
||||||
userId={member?.uid!}
|
userId={member?.uid!}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!caregivers.length && (
|
{!!caregivers.length && (
|
||||||
<>
|
<View style={styles.card}>
|
||||||
<Text text70 marginB-10 marginT-15>
|
<Text style={styles.subTit} marginB-10 marginT-15>
|
||||||
Caregivers
|
Caregivers
|
||||||
</Text>
|
</Text>
|
||||||
{caregivers?.map((member) => (
|
{caregivers?.map((member) => (
|
||||||
@ -196,7 +224,7 @@ const MyGroup = () => {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!!familyDevices.length && (
|
{!!familyDevices.length && (
|
||||||
@ -237,19 +265,8 @@ const MyGroup = () => {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
<FloatingButton
|
|
||||||
fullWidth
|
|
||||||
hideBackgroundOverlay
|
|
||||||
visible
|
|
||||||
button={{
|
|
||||||
label: "+ Add a user device",
|
|
||||||
onPress: () => setShowNewUserInfoDialog(true),
|
|
||||||
style: styles.bottomButton,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
visible={showAddUserDialog}
|
visible={showAddUserDialog}
|
||||||
@ -302,8 +319,8 @@ const MyGroup = () => {
|
|||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
panDirection={PanningProvider.Directions.DOWN}
|
panDirection={PanningProvider.Directions.DOWN}
|
||||||
visible={showNewUserInfoDialog}
|
visible={onNewUserClick}
|
||||||
onDismiss={() => setShowNewUserInfoDialog(false)}
|
onDismiss={() => setOnNewUserClick(false)}
|
||||||
>
|
>
|
||||||
<PreviousNextView>
|
<PreviousNextView>
|
||||||
<KeyboardAwareScrollView>
|
<KeyboardAwareScrollView>
|
||||||
@ -312,15 +329,25 @@ const MyGroup = () => {
|
|||||||
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 16}}>
|
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 16}}>
|
||||||
New User Information
|
New User Information
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity onPress={() => {
|
<TouchableOpacity
|
||||||
setShowNewUserInfoDialog(false)
|
onPress={() => {
|
||||||
}}>
|
setOnNewUserClick(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CircledXIcon/>
|
<CircledXIcon/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.divider} spread/>
|
<View style={styles.divider} spread/>
|
||||||
|
|
||||||
<View row centerV gap-20 marginV-20>
|
<View row centerV gap-20 marginV-20>
|
||||||
|
{pfpUri ? (
|
||||||
|
<Image
|
||||||
|
height={65.54}
|
||||||
|
width={65.54}
|
||||||
|
style={{borderRadius: 25}}
|
||||||
|
source={{uri: pfpUri}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<View
|
<View
|
||||||
height={65.54}
|
height={65.54}
|
||||||
width={65.54}
|
width={65.54}
|
||||||
@ -331,12 +358,22 @@ const MyGroup = () => {
|
|||||||
style={{borderRadius: 25}}
|
style={{borderRadius: 25}}
|
||||||
center
|
center
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => {
|
)}
|
||||||
}}>
|
|
||||||
|
{pfpUri ? (
|
||||||
|
<TouchableOpacity onPress={handleClearImage}>
|
||||||
|
<Text color={Colors.red40} style={styles.jakarta13} marginL-15>
|
||||||
|
Clear user photo
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : (
|
||||||
|
<TouchableOpacity onPress={pickImage}>
|
||||||
<Text color="#50be0c" style={styles.jakarta13} marginL-15>
|
<Text color="#50be0c" style={styles.jakarta13} marginL-15>
|
||||||
Upload User Profile Photo
|
Upload User Profile Photo
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.jakarta12}>Member Status</Text>
|
<Text style={styles.jakarta12}>Member Status</Text>
|
||||||
@ -349,21 +386,30 @@ const MyGroup = () => {
|
|||||||
floatingPlaceholder
|
floatingPlaceholder
|
||||||
style={styles.inViewPicker}
|
style={styles.inViewPicker}
|
||||||
trailingAccessory={
|
trailingAccessory={
|
||||||
<View style={{
|
<View
|
||||||
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
marginTop: -38,
|
marginTop: -38,
|
||||||
paddingRight: 15
|
paddingRight: 15,
|
||||||
}}>
|
}}
|
||||||
<Ionicons name={"chevron-down"} style={{alignSelf: "center"}} size={20}
|
>
|
||||||
color={"#000000"}/>
|
<Ionicons
|
||||||
|
name={"chevron-down"}
|
||||||
|
style={{alignSelf: "center"}}
|
||||||
|
size={20}
|
||||||
|
color={"#000000"}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Picker.Item label="Child" value={ProfileType.CHILD}/>
|
<Picker.Item label="Child" value={ProfileType.CHILD}/>
|
||||||
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
|
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
|
||||||
<Picker.Item label="Caregiver" value={ProfileType.CAREGIVER}/>
|
<Picker.Item
|
||||||
|
label="Caregiver"
|
||||||
|
value={ProfileType.CAREGIVER}
|
||||||
|
/>
|
||||||
<Picker.Item
|
<Picker.Item
|
||||||
label="Family Device"
|
label="Family Device"
|
||||||
value={ProfileType.FAMILY_DEVICE}
|
value={ProfileType.FAMILY_DEVICE}
|
||||||
@ -387,7 +433,7 @@ const MyGroup = () => {
|
|||||||
onChangeText={setFirstName}
|
onChangeText={setFirstName}
|
||||||
style={styles.inputField}
|
style={styles.inputField}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
lNameRef.current?.focus()
|
lNameRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
@ -404,11 +450,10 @@ const MyGroup = () => {
|
|||||||
onChangeText={setLastName}
|
onChangeText={setLastName}
|
||||||
style={styles.inputField}
|
style={styles.inputField}
|
||||||
onSubmitEditing={() => {
|
onSubmitEditing={() => {
|
||||||
emailRef.current?.focus()
|
emailRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -470,12 +515,14 @@ const styles = StyleSheet.create({
|
|||||||
marginVertical: 15,
|
marginVertical: 15,
|
||||||
backgroundColor: "white",
|
backgroundColor: "white",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
borderRadius: 15,
|
borderRadius: 12,
|
||||||
padding: 20,
|
paddingHorizontal: 21,
|
||||||
|
paddingVertical: 20,
|
||||||
},
|
},
|
||||||
bottomButton: {
|
bottomButton: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 80,
|
bottom: 50,
|
||||||
|
backgroundColor: "#e8156c",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
},
|
},
|
||||||
familyCard: {
|
familyCard: {
|
||||||
@ -556,6 +603,16 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
},
|
},
|
||||||
|
pfp: {aspectRatio: 1, width: 37.03, borderRadius: 10.56},
|
||||||
|
userType: {
|
||||||
|
fontFamily: "Manrope_500Medium",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#858585",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MyGroup;
|
export default MyGroup;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { StyleSheet, TouchableOpacity } from "react-native";
|
|||||||
import { ScrollView } from "react-native-gesture-handler";
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Colors,
|
Colors,
|
||||||
Image,
|
Image,
|
||||||
Picker,
|
Picker,
|
||||||
@ -18,6 +19,7 @@ import { useAuthContext } from "@/contexts/AuthContext";
|
|||||||
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
|
||||||
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
|
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
|
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
|
||||||
|
|
||||||
const MyProfile = () => {
|
const MyProfile = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
@ -32,6 +34,15 @@ const MyProfile = () => {
|
|||||||
string | ImagePicker.ImagePickerAsset | null
|
string | ImagePicker.ImagePickerAsset | null
|
||||||
>(profileData?.pfp || null);
|
>(profileData?.pfp || null);
|
||||||
|
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleHideDeleteDialog = () => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
};
|
||||||
|
const handleShowDeleteDialog = () => {
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
const { mutateAsync: updateUserData } = useUpdateUserData();
|
const { mutateAsync: updateUserData } = useUpdateUserData();
|
||||||
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
@ -48,13 +59,12 @@ const MyProfile = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
debouncedUserDataUpdate();
|
debouncedUserDataUpdate();
|
||||||
}, [timeZone, lastName, firstName, profileImage]);
|
}, [timeZone, lastName, firstName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profileData) {
|
if (profileData) {
|
||||||
setFirstName(profileData.firstName || "");
|
setFirstName(profileData.firstName || "");
|
||||||
setLastName(profileData.lastName || "");
|
setLastName(profileData.lastName || "");
|
||||||
// setProfileImage(profileData.pfp || null);
|
|
||||||
setTimeZone(
|
setTimeZone(
|
||||||
profileData.timeZone || Localization.getCalendars()[0].timeZone!
|
profileData.timeZone || Localization.getCalendars()[0].timeZone!
|
||||||
);
|
);
|
||||||
@ -78,7 +88,7 @@ const MyProfile = () => {
|
|||||||
|
|
||||||
if (!result.canceled) {
|
if (!result.canceled) {
|
||||||
setProfileImage(result.assets[0].uri);
|
setProfileImage(result.assets[0].uri);
|
||||||
changeProfilePicture(result.assets[0]);
|
await changeProfilePicture(result.assets[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -93,7 +103,7 @@ const MyProfile = () => {
|
|||||||
: profileImage;
|
: profileImage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{ paddingBottom: 100, flex: 1 }}>
|
<ScrollView style={{ paddingBottom: 20, flex: 1 }}>
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.subTit}>Your Profile</Text>
|
<Text style={styles.subTit}>Your Profile</Text>
|
||||||
<View row spread paddingH-15 centerV marginV-15>
|
<View row spread paddingH-15 centerV marginV-15>
|
||||||
@ -205,6 +215,22 @@ const MyProfile = () => {
|
|||||||
</Picker>
|
</Picker>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
backgroundColor="#FF5449"
|
||||||
|
label="Delete Profile"
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 15 }}
|
||||||
|
onPress={handleShowDeleteDialog}
|
||||||
|
/>
|
||||||
|
<DeleteProfileDialogs
|
||||||
|
onFirstYes={() => {
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
}}
|
||||||
|
visible={showDeleteDialog}
|
||||||
|
onDismiss={handleHideDeleteDialog}
|
||||||
|
onConfirm={() => {console.log('delete account here')}}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,10 @@ const UserMenu = ({
|
|||||||
panDirection={PanningDirectionsEnum.DOWN}
|
panDirection={PanningDirectionsEnum.DOWN}
|
||||||
>
|
>
|
||||||
<Card padding-20 center>
|
<Card padding-20 center>
|
||||||
<Text marginB-10>Scan this QR Code to Login:</Text>
|
<Text center marginB-10 style={{fontSize: 16, maxWidth: "80%"}}>Ask your family to download the app
|
||||||
|
and then scan the
|
||||||
|
QR Code to join the family group:
|
||||||
|
</Text>
|
||||||
<QRCode value={userId} size={150}/>
|
<QRCode value={userId} size={150}/>
|
||||||
<Button
|
<Button
|
||||||
marginT-20
|
marginT-20
|
||||||
|
|||||||
@ -1,38 +1,46 @@
|
|||||||
import { StyleSheet } from "react-native";
|
import { Dimensions, 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 { AntDesign } from "@expo/vector-icons";
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
import LinearGradient from "react-native-linear-gradient";
|
||||||
import AddChoreDialog from "./AddChoreDialog";
|
import AddChoreDialog from "./AddChoreDialog";
|
||||||
|
import PlusIcon from "@/assets/svgs/PlusIcon";
|
||||||
|
|
||||||
const AddChore = () => {
|
const AddChore = () => {
|
||||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinearGradient
|
<View
|
||||||
colors={["#f9f8f700", "#f9f8f7", "#f9f8f700"]}
|
row
|
||||||
locations={[0, 0.5, 1]}
|
spread
|
||||||
style={styles.gradient}
|
paddingH-20
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 15,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<View style={styles.buttonContainer}>
|
<View style={styles.buttonContainer}>
|
||||||
<Button
|
<Button
|
||||||
marginH-25
|
marginB-30
|
||||||
size={ButtonSize.large}
|
size={ButtonSize.large}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={() => setIsVisible(!isVisible)}
|
onPress={() => setIsVisible(!isVisible)}
|
||||||
>
|
>
|
||||||
<AntDesign name="plus" size={24} color="white" />
|
<PlusIcon />
|
||||||
<Text
|
<Text
|
||||||
white
|
white
|
||||||
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 15 }}
|
style={{ fontFamily: "Manrope_600SemiBold", fontSize: 15 }}
|
||||||
marginL-10
|
marginL-5
|
||||||
>
|
>
|
||||||
Create new to do
|
Create new to do
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
{isVisible && (
|
||||||
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
|
<AddChoreDialog isVisible={isVisible} setIsVisible={setIsVisible} />
|
||||||
</LinearGradient>
|
)}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,8 +61,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: "rgb(253, 23, 117)",
|
backgroundColor: "rgb(253, 23, 117)",
|
||||||
paddingVertical: 15,
|
height: 53.26,
|
||||||
paddingHorizontal: 30,
|
|
||||||
borderRadius: 30,
|
borderRadius: 30,
|
||||||
width: 335,
|
width: 335,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
import { View, Text, Button, Switch, PickerModes } from "react-native-ui-lib";
|
|
||||||
import React, { useRef, useState } from "react";
|
|
||||||
import PointsSlider from "@/components/shared/PointsSlider";
|
|
||||||
import { repeatOptions, useToDosContext } from "@/contexts/ToDosContext";
|
|
||||||
import { Feather, AntDesign, Ionicons } from "@expo/vector-icons";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Button,
|
||||||
TextField,
|
|
||||||
DateTimePicker,
|
|
||||||
Picker,
|
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
|
DateTimePicker,
|
||||||
|
Dialog,
|
||||||
|
Picker,
|
||||||
|
PickerModes,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
TextFieldRef,
|
||||||
|
View
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
import { PanningDirectionsEnum } from "react-native-ui-lib/src/incubator/panView";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import { Dimensions, StyleSheet } from "react-native";
|
import PointsSlider from "@/components/shared/PointsSlider";
|
||||||
|
import {repeatOptions, useToDosContext} from "@/contexts/ToDosContext";
|
||||||
|
import {Ionicons} from "@expo/vector-icons";
|
||||||
|
import {PanningDirectionsEnum} from "react-native-ui-lib/src/incubator/panView";
|
||||||
|
import {Dimensions, KeyboardAvoidingView, StyleSheet} from "react-native";
|
||||||
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
import DropModalIcon from "@/assets/svgs/DropModalIcon";
|
||||||
import { IToDo } from "@/hooks/firebase/types/todoData";
|
import {IToDo} from "@/hooks/firebase/types/todoData";
|
||||||
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
|
import AssigneesDisplay from "@/components/shared/AssigneesDisplay";
|
||||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
import CalendarIcon from "@/assets/svgs/CalendarIcon";
|
import CalendarIcon from "@/assets/svgs/CalendarIcon";
|
||||||
import ClockIcon from "@/assets/svgs/ClockIcon";
|
|
||||||
import ClockOIcon from "@/assets/svgs/ClockOIcon";
|
import ClockOIcon from "@/assets/svgs/ClockOIcon";
|
||||||
import ProfileIcon from "@/assets/svgs/ProfileIcon";
|
import ProfileIcon from "@/assets/svgs/ProfileIcon";
|
||||||
import RepeatFreq from "./RepeatFreq";
|
import RepeatFreq from "./RepeatFreq";
|
||||||
@ -36,20 +40,23 @@ const defaultTodo = {
|
|||||||
rotate: false,
|
rotate: false,
|
||||||
repeatType: "Every week",
|
repeatType: "Every week",
|
||||||
assignees: [],
|
assignees: [],
|
||||||
|
repeatDays: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
||||||
const { addToDo, updateToDo } = useToDosContext();
|
const {addToDo, updateToDo} = useToDosContext();
|
||||||
const [todo, setTodo] = useState<IToDo>(
|
const [todo, setTodo] = useState<IToDo>(
|
||||||
addChoreDialogProps.selectedTodo ?? defaultTodo
|
addChoreDialogProps.selectedTodo ?? defaultTodo
|
||||||
);
|
);
|
||||||
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
|
const [selectedAssignees, setSelectedAssignees] = useState<string[]>(
|
||||||
addChoreDialogProps?.selectedTodo?.assignees ?? []
|
addChoreDialogProps?.selectedTodo?.assignees ?? []
|
||||||
);
|
);
|
||||||
const { width, height } = Dimensions.get("screen");
|
const {width} = Dimensions.get("screen");
|
||||||
const [points, setPoints] = useState<number>(todo.points);
|
const [points, setPoints] = useState<number>(todo.points);
|
||||||
|
|
||||||
const { data: members } = useGetFamilyMembers();
|
const titleRef = useRef<TextFieldRef>(null)
|
||||||
|
|
||||||
|
const {data: members} = useGetFamilyMembers();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setTodo(defaultTodo);
|
setTodo(defaultTodo);
|
||||||
@ -67,6 +74,31 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRepeatDaysChange = (day: string, set: boolean) => {
|
||||||
|
if (set) {
|
||||||
|
const updatedTodo = {
|
||||||
|
...todo,
|
||||||
|
repeatDays: [...todo.repeatDays, day]
|
||||||
|
}
|
||||||
|
setTodo(updatedTodo);
|
||||||
|
} else {
|
||||||
|
const array = todo.repeatDays ?? [];
|
||||||
|
let index = array.indexOf(day);
|
||||||
|
array.splice(index, 1);
|
||||||
|
const updatedTodo = {
|
||||||
|
...todo,
|
||||||
|
repeatDays: array
|
||||||
|
}
|
||||||
|
setTodo(updatedTodo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
titleRef?.current?.focus()
|
||||||
|
}, 500)
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
bottom={true}
|
bottom={true}
|
||||||
@ -110,7 +142,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
updateToDo({
|
updateToDo({
|
||||||
...todo,
|
...todo,
|
||||||
points: points,
|
points: points,
|
||||||
assignees: selectedAssignees,
|
assignees: selectedAssignees
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addToDo({
|
addToDo({
|
||||||
@ -118,6 +150,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
done: false,
|
done: false,
|
||||||
points: points,
|
points: points,
|
||||||
assignees: selectedAssignees,
|
assignees: selectedAssignees,
|
||||||
|
repeatDays: todo.repeatDays ?? []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
@ -127,24 +160,25 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<KeyboardAvoidingView>
|
||||||
<TextField
|
<TextField
|
||||||
placeholder="Add a To Do"
|
placeholder="Add a To Do"
|
||||||
autoFocus
|
|
||||||
value={todo?.title}
|
value={todo?.title}
|
||||||
onChangeText={(text) => {
|
onChangeText={(text) => {
|
||||||
setTodo((oldValue: IToDo) => ({ ...oldValue, title: text }));
|
setTodo((oldValue: IToDo) => ({...oldValue, title: text}));
|
||||||
}}
|
}}
|
||||||
placeholderTextColor="#2d2d30"
|
placeholderTextColor="#2d2d30"
|
||||||
text60R
|
text60R
|
||||||
marginT-15
|
marginT-15
|
||||||
marginL-30
|
marginL-30
|
||||||
|
ref={titleRef}
|
||||||
/>
|
/>
|
||||||
<View style={styles.divider} marginT-8 />
|
<View style={styles.divider} marginT-8/>
|
||||||
<View marginL-30 centerV>
|
<View marginL-30 centerV>
|
||||||
<View row marginB-10>
|
<View row marginB-10>
|
||||||
{todo?.date && (
|
{todo?.date && (
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<CalendarIcon color="#919191" width={24} height={24} />
|
<CalendarIcon color="#919191" width={24} height={24}/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={todo.date}
|
value={todo.date}
|
||||||
text70
|
text70
|
||||||
@ -154,14 +188,14 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
}}
|
}}
|
||||||
marginL-12
|
marginL-12
|
||||||
onChange={(date) => {
|
onChange={(date) => {
|
||||||
setTodo((oldValue: IToDo) => ({ ...oldValue, date: date }));
|
setTodo((oldValue: IToDo) => ({...oldValue, date: date}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View row centerV>
|
<View row centerV>
|
||||||
<ClockOIcon />
|
<ClockOIcon/>
|
||||||
<Picker
|
<Picker
|
||||||
marginL-12
|
marginL-12
|
||||||
placeholder="Select Repeat Type"
|
placeholder="Select Repeat Type"
|
||||||
@ -183,7 +217,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
topBarProps={{ title: "Repeat" }}
|
topBarProps={{title: "Repeat"}}
|
||||||
style={{
|
style={{
|
||||||
marginVertical: 5,
|
marginVertical: 5,
|
||||||
fontFamily: "PlusJakartaSans_500Medium",
|
fontFamily: "PlusJakartaSans_500Medium",
|
||||||
@ -199,30 +233,31 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
))}
|
))}
|
||||||
</Picker>
|
</Picker>
|
||||||
</View>
|
</View>
|
||||||
{todo.repeatType == "Every week" && <RepeatFreq/>}
|
{todo.repeatType == "Every week" && <RepeatFreq handleRepeatDaysChange={handleRepeatDaysChange}
|
||||||
|
repeatDays={todo.repeatDays ?? []}/>}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider}/>
|
||||||
|
|
||||||
<View marginH-30 marginB-10 row centerV>
|
<View marginH-30 marginB-10 row centerV>
|
||||||
<ProfileIcon color="#919191" />
|
<ProfileIcon color="#919191"/>
|
||||||
<Text style={styles.sub} marginL-10>
|
<Text style={styles.sub} marginL-10>
|
||||||
Assignees
|
Assignees
|
||||||
</Text>
|
</Text>
|
||||||
<View flex-1 />
|
<View flex-1/>
|
||||||
<Picker
|
<Picker
|
||||||
marginL-8
|
marginL-8
|
||||||
value={selectedAssignees}
|
value={selectedAssignees}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedAssignees([...selectedAssignees, ...value]);
|
setSelectedAssignees(value);
|
||||||
}}
|
}}
|
||||||
style={{ marginVertical: 5 }}
|
style={{marginVertical: 5}}
|
||||||
mode={PickerModes.MULTI}
|
mode={PickerModes.MULTI}
|
||||||
renderInput={() => (
|
renderInput={() => (
|
||||||
<Button
|
<Button
|
||||||
size={ButtonSize.small}
|
size={ButtonSize.small}
|
||||||
paddingH-8
|
paddingH-8
|
||||||
iconSource={() => (
|
iconSource={() => (
|
||||||
<Ionicons name="add-outline" size={20} color="#ea156c" />
|
<Ionicons name="add-outline" size={20} color="#ea156c"/>
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
@ -233,7 +268,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
}}
|
}}
|
||||||
color="#ea156c"
|
color="#ea156c"
|
||||||
label="Assign"
|
label="Assign"
|
||||||
labelStyle={{ fontFamily: "Manrope_600SemiBold", fontSize: 14 }}
|
labelStyle={{fontFamily: "Manrope_600SemiBold", fontSize: 14}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -253,22 +288,22 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View row centerV style={styles.rotateSwitch}>
|
<View row centerV style={styles.rotateSwitch}>
|
||||||
<Text style={{ fontFamily: "PlusJakartaSans_500Medium", fontSize: 16 }}>
|
<Text style={{fontFamily: "PlusJakartaSans_500Medium", fontSize: 16}}>
|
||||||
Take Turns
|
Take Turns
|
||||||
</Text>
|
</Text>
|
||||||
<Switch
|
<Switch
|
||||||
onColor={"#ea156c"}
|
onColor={"#ea156c"}
|
||||||
value={todo.rotate}
|
value={todo.rotate}
|
||||||
style={{ width: 43.06, height: 27.13 }}
|
style={{width: 43.06, height: 27.13}}
|
||||||
marginL-10
|
marginL-10
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setTodo((oldValue) => ({ ...oldValue, rotate: value }))
|
setTodo((oldValue) => ({...oldValue, rotate: value}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.divider} />
|
<View style={styles.divider}/>
|
||||||
<View marginH-30 marginB-15 row centerV>
|
<View marginH-30 marginB-15 row centerV>
|
||||||
<Ionicons name="gift-outline" size={25} color="#919191" />
|
<Ionicons name="gift-outline" size={25} color="#919191"/>
|
||||||
<Text style={styles.sub} marginL-10>
|
<Text style={styles.sub} marginL-10>
|
||||||
Reward Points
|
Reward Points
|
||||||
</Text>
|
</Text>
|
||||||
@ -278,6 +313,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
setPoints={setPoints}
|
setPoints={setPoints}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -285,7 +321,7 @@ const AddChoreDialog = (addChoreDialogProps: IAddChoreDialog) => {
|
|||||||
export default AddChoreDialog;
|
export default AddChoreDialog;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
divider: { height: 1, backgroundColor: "#e4e4e4", marginVertical: 15 },
|
divider: {height: 1, backgroundColor: "#e4e4e4", marginVertical: 15},
|
||||||
gradient: {
|
gradient: {
|
||||||
height: "25%",
|
height: "25%",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { View, Text, TouchableOpacity, Picker } from "react-native-ui-lib";
|
import { View, Text, TouchableOpacity, Picker } from "react-native-ui-lib";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {DAYS_OF_WEEK_ENUM} from "@/hooks/firebase/types/todoData";
|
||||||
|
|
||||||
const RepeatFreq = () => {
|
const RepeatFreq = ({ repeatDays, handleRepeatDaysChange }: { repeatDays: string[], handleRepeatDaysChange: Function }) => {
|
||||||
const [weeks, setWeeks] = useState<number>(1);
|
const [weeks, setWeeks] = useState<number>(1);
|
||||||
const weekOptions: number[] = Array.from({ length: 52 }, (_, i) => i + 1);
|
const weekOptions: number[] = Array.from({ length: 52 }, (_, i) => i + 1);
|
||||||
|
|
||||||
@ -9,23 +10,30 @@ const RepeatFreq = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View row centerV spread marginR-30>
|
<View row centerV spread marginR-30>
|
||||||
<RepeatOption value={"Monday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.MONDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Tuesday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.TUESDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Wednesday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.WEDNESDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Thirsday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.THURSDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Friday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.FRIDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Saturday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.SATURDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
<RepeatOption value={"Sunday"} />
|
<RepeatOption value={DAYS_OF_WEEK_ENUM.SUNDAY} handleRepeatDaysChange={handleRepeatDaysChange} repeatDays={repeatDays}/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepeatFreq;
|
export default RepeatFreq;
|
||||||
|
|
||||||
const RepeatOption = ({ value }: { value: string }) => {
|
const RepeatOption = ({ value, handleRepeatDaysChange, repeatDays }: { value: string, handleRepeatDaysChange: Function, repeatDays: string[] }) => {
|
||||||
const [isSet, setisSet] = useState(false);
|
const [isSet, setisSet] = useState(repeatDays.includes(value));
|
||||||
|
|
||||||
|
|
||||||
|
const handleDayChange = () => {
|
||||||
|
handleRepeatDaysChange(value, !isSet)
|
||||||
|
setisSet(!isSet);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={() => setisSet(!isSet)}>
|
<TouchableOpacity onPress={handleDayChange}>
|
||||||
<View
|
<View
|
||||||
center
|
center
|
||||||
marginT-8
|
marginT-8
|
||||||
|
|||||||
@ -1,25 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Checkbox,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Dialog,
|
Dialog,
|
||||||
Button,
|
Button,
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
|
Checkbox,
|
||||||
} from "react-native-ui-lib";
|
} from "react-native-ui-lib";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useToDosContext } from "@/contexts/ToDosContext";
|
import { useToDosContext } from "@/contexts/ToDosContext";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import PointsSlider from "@/components/shared/PointsSlider";
|
import PointsSlider from "@/components/shared/PointsSlider";
|
||||||
import { IToDo } from "@/hooks/firebase/types/todoData";
|
import { IToDo } from "@/hooks/firebase/types/todoData";
|
||||||
import { ImageBackground } from "react-native";
|
import { ImageBackground, StyleSheet } from "react-native";
|
||||||
import AddChoreDialog from "@/components/pages/todos/AddChoreDialog";
|
import AddChoreDialog from "@/components/pages/todos/AddChoreDialog";
|
||||||
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
import RepeatIcon from "@/assets/svgs/RepeatIcon";
|
import RepeatIcon from "@/assets/svgs/RepeatIcon";
|
||||||
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
|
const ToDoItem = (props: {
|
||||||
|
item: IToDo;
|
||||||
|
isSettings?: boolean;
|
||||||
|
is7Days?: boolean;
|
||||||
|
}) => {
|
||||||
const { updateToDo } = useToDosContext();
|
const { updateToDo } = useToDosContext();
|
||||||
const { data: members } = useGetFamilyMembers();
|
const { data: members } = useGetFamilyMembers();
|
||||||
|
const { profileData } = useAuthContext();
|
||||||
|
const isParent = profileData?.userType === ProfileType.PARENT;
|
||||||
|
|
||||||
const [visible, setVisible] = useState<boolean>(false);
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
const [points, setPoints] = useState(props.item.points);
|
const [points, setPoints] = useState(props.item.points);
|
||||||
const [pointsModalVisible, setPointsModalVisible] = useState<boolean>(false);
|
const [pointsModalVisible, setPointsModalVisible] = useState<boolean>(false);
|
||||||
@ -41,16 +51,24 @@ const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
|
|||||||
const selectedMembers = members?.filter((x) =>
|
const selectedMembers = members?.filter((x) =>
|
||||||
props?.item?.assignees?.includes(x?.uid!)
|
props?.item?.assignees?.includes(x?.uid!)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let isTodoEditable;
|
||||||
|
if (isParent) {
|
||||||
|
isTodoEditable = true;
|
||||||
|
} else {
|
||||||
|
isTodoEditable = props.item.creatorId === profileData?.uid;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
key={props.item.id}
|
||||||
centerV
|
centerV
|
||||||
paddingV-10
|
paddingV-10
|
||||||
paddingH-13
|
paddingH-13
|
||||||
marginV-10
|
marginV-10
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 17,
|
borderRadius: 17,
|
||||||
backgroundColor: props.item.done ? "#e0e0e0" : "white",
|
backgroundColor: "white",
|
||||||
opacity: props.item.done ? 0.3 : 1,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visible && (
|
{visible && (
|
||||||
@ -66,23 +84,21 @@ const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
|
|||||||
style={{
|
style={{
|
||||||
textDecorationLine: props.item.done ? "line-through" : "none",
|
textDecorationLine: props.item.done ? "line-through" : "none",
|
||||||
fontFamily: "Manrope_500Medium",
|
fontFamily: "Manrope_500Medium",
|
||||||
|
color: props.item.done? "#a09f9f": "black",
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
}}
|
}}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setVisible(true);
|
isTodoEditable ? setVisible(true) : null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.item.title}
|
{props.item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
value={props.item.done}
|
value={props.item.done}
|
||||||
containerStyle={{
|
containerStyle={[styles.checkbox, { borderRadius: 50 }]}
|
||||||
borderWidth: 0.7,
|
style={styles.checked}
|
||||||
borderRadius: 50,
|
size={26.64}
|
||||||
borderColor: "gray",
|
borderRadius={50}
|
||||||
height: 24.64,
|
|
||||||
width: 24.64,
|
|
||||||
}}
|
|
||||||
color="#fd1575"
|
color="#fd1575"
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
updateToDo({ id: props.item.id, done: !props.item.done });
|
updateToDo({ id: props.item.id, done: !props.item.done });
|
||||||
@ -146,6 +162,9 @@ const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
|
|||||||
return props.item.repeatType;
|
return props.item.repeatType;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
|
{props.item.date &&
|
||||||
|
props.is7Days &&
|
||||||
|
" / " + format(props.item.date, "EEEE")}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -246,3 +265,17 @@ const ToDoItem = (props: { item: IToDo; isSettings?: boolean }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ToDoItem;
|
export default ToDoItem;
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
checkbox: {
|
||||||
|
borderRadius: 50,
|
||||||
|
borderWidth: 0.7,
|
||||||
|
color: "#bfbfbf",
|
||||||
|
borderColor: "#bfbfbf",
|
||||||
|
width: 24.64,
|
||||||
|
aspectRatio: 1,
|
||||||
|
},
|
||||||
|
checked: {
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@ -14,8 +14,10 @@ import { IToDo } from "@/hooks/firebase/types/todoData";
|
|||||||
|
|
||||||
const groupToDosByDate = (toDos: IToDo[]) => {
|
const groupToDosByDate = (toDos: IToDo[]) => {
|
||||||
let sortedTodos = toDos.sort((a, b) => a.date - b.date);
|
let sortedTodos = toDos.sort((a, b) => a.date - b.date);
|
||||||
return sortedTodos.reduce((groups, toDo) => {
|
return sortedTodos.reduce(
|
||||||
|
(groups, toDo) => {
|
||||||
let dateKey;
|
let dateKey;
|
||||||
|
let subDateKey;
|
||||||
|
|
||||||
const isNext7Days = (date: Date) => {
|
const isNext7Days = (date: Date) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@ -24,7 +26,10 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
|||||||
|
|
||||||
const isNext30Days = (date: Date) => {
|
const isNext30Days = (date: Date) => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
return isWithinInterval(date, { start: today, end: addDays(today, 30) });
|
return isWithinInterval(date, {
|
||||||
|
start: today,
|
||||||
|
end: addDays(today, 30),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toDo.date === null) {
|
if (toDo.date === null) {
|
||||||
@ -37,17 +42,36 @@ const groupToDosByDate = (toDos: IToDo[]) => {
|
|||||||
dateKey = "Next 7 Days";
|
dateKey = "Next 7 Days";
|
||||||
} else if (isNext30Days(toDo.date)) {
|
} else if (isNext30Days(toDo.date)) {
|
||||||
dateKey = "Next 30 Days";
|
dateKey = "Next 30 Days";
|
||||||
|
subDateKey = format(toDo.date, "MMM d");
|
||||||
} else {
|
} else {
|
||||||
dateKey = format(toDo.date, "EEE MMM dd");
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groups[dateKey]) {
|
if (!groups[dateKey]) {
|
||||||
groups[dateKey] = [];
|
groups[dateKey] = {
|
||||||
|
items: [],
|
||||||
|
subgroups: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateKey === "Next 30 Days" && subDateKey) {
|
||||||
|
if (!groups[dateKey].subgroups[subDateKey]) {
|
||||||
|
groups[dateKey].subgroups[subDateKey] = [];
|
||||||
|
}
|
||||||
|
groups[dateKey].subgroups[subDateKey].push(toDo);
|
||||||
|
} else {
|
||||||
|
groups[dateKey].items.push(toDo);
|
||||||
}
|
}
|
||||||
|
|
||||||
groups[dateKey].push(toDo);
|
|
||||||
return groups;
|
return groups;
|
||||||
}, {} as { [key: string]: IToDo[] });
|
},
|
||||||
|
{} as {
|
||||||
|
[key: string]: {
|
||||||
|
items: IToDo[];
|
||||||
|
subgroups: { [key: string]: IToDo[] };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ToDosList = ({ isSettings }: { isSettings?: boolean }) => {
|
const ToDosList = ({ isSettings }: { isSettings?: boolean }) => {
|
||||||
@ -67,59 +91,95 @@ const ToDosList = ({ isSettings }: { isSettings?: boolean }) => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const noDateToDos = groupedToDos["No Date"] || [];
|
const noDateToDos = groupedToDos["No Date"]?.items || [];
|
||||||
const datedToDos = Object.keys(groupedToDos).filter(
|
const datedToDos = Object.keys(groupedToDos).filter(
|
||||||
(key) => key !== "No Date"
|
(key) => key !== "No Date"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderTodoGroup = (dateKey: string) => {
|
||||||
|
const isExpanded = expandedGroups[dateKey] || false;
|
||||||
|
|
||||||
|
if (dateKey === "Next 30 Days") {
|
||||||
|
const subgroups = Object.entries(groupedToDos[dateKey].subgroups).sort(
|
||||||
|
([dateA], [dateB]) => {
|
||||||
|
const dateAObj = new Date(dateA);
|
||||||
|
const dateBObj = new Date(dateB);
|
||||||
|
return dateAObj.getTime() - dateBObj.getTime();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View marginB-402>
|
<View key={dateKey}>
|
||||||
{noDateToDos.length > 0 && (
|
<TouchableOpacity
|
||||||
<View key="No Date">
|
onPress={() => toggleExpand(dateKey)}
|
||||||
<View row spread paddingH-19 marginB-12>
|
|
||||||
<Text
|
|
||||||
text70
|
|
||||||
style={{
|
style={{
|
||||||
fontWeight: "bold",
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
marginBottom: 4,
|
||||||
|
marginTop: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Unscheduled
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: "Manrope_700Bold",
|
||||||
|
fontSize: 15,
|
||||||
|
color: "#484848",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dateKey}
|
||||||
</Text>
|
</Text>
|
||||||
{!expandNoDate && (
|
|
||||||
<AntDesign
|
<AntDesign
|
||||||
name="caretright"
|
name={isExpanded ? "caretdown" : "caretright"}
|
||||||
size={24}
|
size={24}
|
||||||
color="#fd1575"
|
color="#fd1575"
|
||||||
onPress={() => {
|
|
||||||
setExpandNoDate(!expandNoDate);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</TouchableOpacity>
|
||||||
{expandNoDate && (
|
|
||||||
<AntDesign
|
{isExpanded &&
|
||||||
name="caretdown"
|
subgroups.map(([subDate, items]) => {
|
||||||
size={24}
|
const sortedItems = [
|
||||||
color="#fd1575"
|
...items.filter((toDo) => !toDo.done),
|
||||||
onPress={() => {
|
...items.filter((toDo) => toDo.done),
|
||||||
setExpandNoDate(!expandNoDate);
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View key={subDate} marginT-15>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
)}
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: "Manrope_600SemiBold",
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#919191",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subDate}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{expandNoDate &&
|
|
||||||
noDateToDos
|
{sortedItems.map((item) => (
|
||||||
.sort((a, b) => Number(a.done) - Number(b.done))
|
<ToDoItem
|
||||||
.map((item) => (
|
isSettings={isSettings}
|
||||||
<ToDoItem isSettings={isSettings} key={item.id} item={item} />
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
is7Days={false}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{datedToDos.map((dateKey) => {
|
|
||||||
const isExpanded = expandedGroups[dateKey] || false;
|
|
||||||
const sortedToDos = [
|
const sortedToDos = [
|
||||||
...groupedToDos[dateKey].filter((toDo) => !toDo.done),
|
...groupedToDos[dateKey].items.filter((toDo) => !toDo.done),
|
||||||
...groupedToDos[dateKey].filter((toDo) => toDo.done),
|
...groupedToDos[dateKey].items.filter((toDo) => toDo.done),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -144,21 +204,58 @@ const ToDosList = ({ isSettings }: { isSettings?: boolean }) => {
|
|||||||
>
|
>
|
||||||
{dateKey}
|
{dateKey}
|
||||||
</Text>
|
</Text>
|
||||||
{!isExpanded && (
|
<AntDesign
|
||||||
<AntDesign name="caretright" size={24} color="#fd1575" />
|
name={isExpanded ? "caretdown" : "caretright"}
|
||||||
)}
|
size={24}
|
||||||
{isExpanded && (
|
color="#fd1575"
|
||||||
<AntDesign name="caretdown" size={24} color="#fd1575" />
|
/>
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{isExpanded &&
|
{isExpanded &&
|
||||||
sortedToDos.map((item) => (
|
sortedToDos.map((item) => (
|
||||||
<ToDoItem isSettings={isSettings} key={item.id} item={item} />
|
<ToDoItem
|
||||||
|
isSettings={isSettings}
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
is7Days={dateKey === "Next 7 Days"}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View marginB-402>
|
||||||
|
{noDateToDos.length > 0 && (
|
||||||
|
<View key="No Date">
|
||||||
|
<View row spread paddingH-19 marginB-12>
|
||||||
|
<Text
|
||||||
|
text70
|
||||||
|
style={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Unscheduled
|
||||||
|
</Text>
|
||||||
|
<AntDesign
|
||||||
|
name={expandNoDate ? "caretdown" : "caretright"}
|
||||||
|
size={24}
|
||||||
|
color="#fd1575"
|
||||||
|
onPress={() => {
|
||||||
|
setExpandNoDate(!expandNoDate);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
{expandNoDate &&
|
||||||
|
noDateToDos
|
||||||
|
.sort((a, b) => Number(a.done) - Number(b.done))
|
||||||
|
.map((item) => (
|
||||||
|
<ToDoItem isSettings={isSettings} key={item.id} item={item} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{datedToDos.map(renderTodoGroup)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,40 +1,48 @@
|
|||||||
import {Button, Text, View} from "react-native-ui-lib";
|
import { Button, Text, View } from "react-native-ui-lib";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
import HeaderTemplate from "@/components/shared/HeaderTemplate";
|
||||||
import AddChore from "./AddChore";
|
import AddChore from "./AddChore";
|
||||||
import ProgressCard from "./ProgressCard";
|
import ProgressCard from "./ProgressCard";
|
||||||
import ToDosList from "./ToDosList";
|
import ToDosList from "./ToDosList";
|
||||||
import {Dimensions, ScrollView, StyleSheet} from "react-native";
|
import { Dimensions, ScrollView, StyleSheet } from "react-native";
|
||||||
import {TouchableOpacity} from "react-native-gesture-handler";
|
import { TouchableOpacity } from "react-native-gesture-handler";
|
||||||
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import FamilyChoresProgress from "./family-chores/FamilyChoresProgress";
|
import FamilyChoresProgress from "./family-chores/FamilyChoresProgress";
|
||||||
import UserChoresProgress from "./user-chores/UserChoresProgress";
|
import UserChoresProgress from "./user-chores/UserChoresProgress";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { toDosPageIndex } from "../calendar/atoms";
|
||||||
|
|
||||||
const ToDosPage = () => {
|
const ToDosPage = () => {
|
||||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
const { profileData } = useAuthContext();
|
||||||
const {profileData} = useAuthContext();
|
const [pageIndex, setPageIndex] = useAtom(toDosPageIndex);
|
||||||
const {width, height} = Dimensions.get("screen");
|
|
||||||
|
const { width, height } = Dimensions.get("screen");
|
||||||
const pageLink = (
|
const pageLink = (
|
||||||
<TouchableOpacity onPress={() => setPageIndex(1)}>
|
<TouchableOpacity onPress={() => setPageIndex(1)}>
|
||||||
<Text color="#ea156d" style={{fontSize: 14}}>
|
<Text color="#ea156d" style={{ fontSize: 14 }}>
|
||||||
View family progress
|
View family progress
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View paddingH-25 backgroundColor="#f9f8f7" height={"100%"} width={width}>
|
|
||||||
{pageIndex == 0 && (
|
|
||||||
<View>
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
|
<View
|
||||||
|
paddingH-25
|
||||||
|
height={"100%"}
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
{pageIndex == 0 && (
|
||||||
|
<View>
|
||||||
<View>
|
<View>
|
||||||
<HeaderTemplate
|
<HeaderTemplate
|
||||||
message="Here are your To Do's"
|
message="Here are your To Do's"
|
||||||
isWelcome={true}
|
isWelcome={true}
|
||||||
link={profileData?.userType == ProfileType.PARENT && pageLink}
|
link={profileData?.userType == ProfileType.PARENT && pageLink}
|
||||||
|
isToDos={true}
|
||||||
/>
|
/>
|
||||||
{profileData?.userType == ProfileType.CHILD && (
|
{profileData?.userType == ProfileType.CHILD && (
|
||||||
<View marginB-25>
|
<View marginB-25>
|
||||||
@ -57,20 +65,19 @@ const ToDosPage = () => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<ToDosList/>
|
<ToDosList />
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{pageIndex == 1 && <FamilyChoresProgress setPageIndex={setPageIndex}/>}
|
{pageIndex == 1 && (
|
||||||
{pageIndex == 2 && <UserChoresProgress setPageIndex={setPageIndex}/>}
|
<FamilyChoresProgress setPageIndex={setPageIndex} />
|
||||||
|
)}
|
||||||
|
{pageIndex == 2 && <UserChoresProgress setPageIndex={setPageIndex} />}
|
||||||
</View>
|
</View>
|
||||||
{
|
</ScrollView>
|
||||||
profileData?.userType == ProfileType.PARENT && <AddChore/>
|
<AddChore />
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import React from "react";
|
|||||||
import { ImageBackground, StyleSheet } from "react-native";
|
import { ImageBackground, StyleSheet } from "react-native";
|
||||||
import FamilyChart from "./FamilyChart";
|
import FamilyChart from "./FamilyChart";
|
||||||
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
|
import { TouchableOpacity } from "react-native-ui-lib/src/incubator";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
|
||||||
const FamilyChoresProgress = ({
|
const FamilyChoresProgress = ({
|
||||||
setPageIndex,
|
setPageIndex,
|
||||||
@ -12,7 +13,20 @@ const FamilyChoresProgress = ({
|
|||||||
return (
|
return (
|
||||||
<View marginT-20 marginH-5>
|
<View marginT-20 marginH-5>
|
||||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<Text style={{ fontFamily: "Manrope_200", fontSize: 12 }}>Back to ToDos</Text>
|
<View row marginT-4 marginB-10 centerV>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={14}
|
||||||
|
color="#979797"
|
||||||
|
style={{ paddingBottom: 3 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
||||||
|
color="#979797"
|
||||||
|
>
|
||||||
|
Return to To Do's
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View centerH>
|
<View centerH>
|
||||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>
|
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 19 }}>
|
||||||
|
|||||||
@ -30,7 +30,20 @@ const UserChoresProgress = ({
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
<TouchableOpacity onPress={() => setPageIndex(0)}>
|
||||||
<Text style={{ fontSize: 14 }}>Back to ToDos</Text>
|
<View row marginT-4 marginB-10 centerV>
|
||||||
|
<Ionicons
|
||||||
|
name="chevron-back"
|
||||||
|
size={14}
|
||||||
|
color="#979797"
|
||||||
|
style={{ paddingBottom: 3 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
|
||||||
|
color="#979797"
|
||||||
|
>
|
||||||
|
Return to To Do's
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
<Text style={{ fontFamily: "Manrope_700Bold", fontSize: 20 }}>
|
||||||
|
|||||||
@ -1,18 +1,37 @@
|
|||||||
import { Image, Text, View } from "react-native-ui-lib";
|
import { Image, Text, View } from "react-native-ui-lib";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import { StyleSheet } from "react-native";
|
import { StyleSheet } from "react-native";
|
||||||
import { colorMap } from "@/constants/colorMap";
|
import { colorMap } from "@/constants/colorMap";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { isFamilyViewAtom } from "../pages/calendar/atoms";
|
||||||
|
import { useGetChildrenByParentId } from "@/hooks/firebase/useGetChildrenByParentId";
|
||||||
|
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
|
||||||
|
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
|
||||||
|
import { child } from "@react-native-firebase/storage";
|
||||||
|
import CachedImage from 'expo-cached-image'
|
||||||
|
|
||||||
const HeaderTemplate = (props: {
|
const HeaderTemplate = (props: {
|
||||||
message: string;
|
message: string;
|
||||||
isWelcome: boolean;
|
isWelcome: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
link?: React.ReactNode;
|
link?: React.ReactNode;
|
||||||
|
isCalendar?: boolean;
|
||||||
|
isToDos?: boolean;
|
||||||
|
isBrainDump?: boolean;
|
||||||
|
isGroceries?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
|
|
||||||
const headerHeight: number = 72;
|
const { data: members } = useGetFamilyMembers();
|
||||||
|
const [children, setChildren] = useState<UserProfile[]>([]);
|
||||||
|
const [isFamilyView] = useAtom(isFamilyViewAtom);
|
||||||
|
|
||||||
|
const headerHeight: number =
|
||||||
|
(props.isCalendar && 65.54) ||
|
||||||
|
(props.isToDos && 84) ||
|
||||||
|
(props.isGroceries && 72.09) ||
|
||||||
|
65.54;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
pfp: {
|
pfp: {
|
||||||
@ -26,14 +45,71 @@ const HeaderTemplate = (props: {
|
|||||||
pfpTxt: {
|
pfpTxt: {
|
||||||
fontFamily: "Manrope_500Medium",
|
fontFamily: "Manrope_500Medium",
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
color: 'white',
|
color: "white",
|
||||||
|
},
|
||||||
|
childrenPfpArr: {
|
||||||
|
width: 65.54,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: -12.44,
|
||||||
|
left: (children.length > 3 && -9) || 0,
|
||||||
|
height: 27.32,
|
||||||
|
},
|
||||||
|
childrenPfp: {
|
||||||
|
aspectRatio: 1,
|
||||||
|
width: 27.32,
|
||||||
|
backgroundColor: "#fd1575",
|
||||||
|
borderRadius: 50,
|
||||||
|
position: "absolute",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#f2f2f2",
|
||||||
|
},
|
||||||
|
bottomMarg: {
|
||||||
|
marginBottom: isFamilyView ? 30 : 15,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (members) {
|
||||||
|
const childrenMembers = members.filter(
|
||||||
|
(member) => member.userType === ProfileType.CHILD
|
||||||
|
);
|
||||||
|
setChildren(childrenMembers);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View row centerV marginV-15>
|
<View row centerV marginV-15 style={styles.bottomMarg}>
|
||||||
{profileData?.pfp ? (
|
{profileData?.pfp ? (
|
||||||
<Image source={{ uri: profileData.pfp }} style={styles.pfp} />
|
<View>
|
||||||
|
<CachedImage source={{ uri: profileData.pfp, }} style={styles.pfp} cacheKey={profileData.pfp}/>
|
||||||
|
{isFamilyView && props.isCalendar && (
|
||||||
|
<View style={styles.childrenPfpArr} row>
|
||||||
|
{children?.slice(0, 3).map((child, index) => {
|
||||||
|
return child.pfp ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: child.pfp }}
|
||||||
|
style={[styles.childrenPfp, { left: index * 19 }]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[styles.childrenPfp, { left: index * 19 }]}
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<Text style={{ color: "white" }}>
|
||||||
|
{child?.firstName?.at(0)}
|
||||||
|
{child?.firstName?.at(1)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{children?.length > 3 && (
|
||||||
|
<View style={[styles.childrenPfp, { left: 3 * 19 }]} center>
|
||||||
|
<Text style={{ color: "white", fontFamily: "Manrope_600SemiBold", fontSize: 12 }}>+{children.length - 3}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.pfp} center>
|
<View style={styles.pfp} center>
|
||||||
<Text style={styles.pfpTxt}>
|
<Text style={styles.pfpTxt}>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { View } from "react-native-ui-lib";
|
|||||||
const RemoveAssigneeBtn = () => {
|
const RemoveAssigneeBtn = () => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.removeBtn} center>
|
<View style={styles.removeBtn} center>
|
||||||
<CloseXIcon />
|
<CloseXIcon width={9} height={9} strokeWidth={2} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
|||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import * as Device from 'expo-device';
|
import * as Device from 'expo-device';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
import { Platform } from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import {useQueryClient} from "react-query";
|
import {useQueryClient} from "react-query";
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ interface IAuthContext {
|
|||||||
profileType?: ProfileType,
|
profileType?: ProfileType,
|
||||||
profileData?: UserProfile,
|
profileData?: UserProfile,
|
||||||
setProfileData: (profileData: UserProfile) => void,
|
setProfileData: (profileData: UserProfile) => void,
|
||||||
|
setRedirectOverride: (val: boolean) => void,
|
||||||
refreshProfileData: () => Promise<void>
|
refreshProfileData: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,16 +51,16 @@ async function registerForPushNotificationsAsync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Device.isDevice) {
|
if (Device.isDevice) {
|
||||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
const {status: existingStatus} = await Notifications.getPermissionsAsync();
|
||||||
let finalStatus = existingStatus;
|
let finalStatus = existingStatus;
|
||||||
|
|
||||||
if (existingStatus !== 'granted') {
|
if (existingStatus !== 'granted') {
|
||||||
const { status } = await Notifications.requestPermissionsAsync();
|
const {status} = await Notifications.requestPermissionsAsync();
|
||||||
finalStatus = status;
|
finalStatus = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalStatus !== 'granted') {
|
if (finalStatus !== 'granted') {
|
||||||
alert('Failed to get push token for push notification!');
|
// alert('Failed to get push token for push notification!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,15 +73,15 @@ async function registerForPushNotificationsAsync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
|
const token = (await Notifications.getExpoPushTokenAsync({projectId})).data;
|
||||||
console.log('Push Token:', token);
|
console.log('Push Token:', token);
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error getting push token: ${error}`);
|
// alert(`Error getting push token: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Must use a physical device for push notifications');
|
// alert('Must use a physical device for push notifications');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,14 +92,17 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
const [initializing, setInitializing] = useState(true);
|
const [initializing, setInitializing] = useState(true);
|
||||||
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
const [profileType, setProfileType] = useState<ProfileType | undefined>(undefined);
|
||||||
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
const [profileData, setProfileData] = useState<UserProfile | undefined>(undefined);
|
||||||
|
const [redirectOverride, setRedirectOverride] = useState(false);
|
||||||
|
|
||||||
const {replace} = useRouter();
|
const {replace} = useRouter();
|
||||||
const ready = !initializing;
|
const ready = !initializing;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
const onAuthStateChangedHandler = async (authUser: FirebaseAuthTypes.User | null) => {
|
||||||
setUser(authUser);
|
if (!redirectOverride) {
|
||||||
|
|
||||||
|
setUser(authUser);
|
||||||
|
|
||||||
if (authUser) {
|
if (authUser) {
|
||||||
await refreshProfileData(authUser);
|
await refreshProfileData(authUser);
|
||||||
@ -109,6 +113,7 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (initializing) setInitializing(false);
|
if (initializing) setInitializing(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
const refreshProfileData = async (user?: FirebaseAuthTypes.User) => {
|
||||||
@ -152,12 +157,12 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}, [initializing]);
|
}, [initializing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ready && user) {
|
if (ready && user && !redirectOverride) {
|
||||||
replace({pathname: "/(auth)/calendar"});
|
replace({pathname: "/(auth)/calendar"});
|
||||||
} else if (ready && !user) {
|
} else if (ready && !user && !redirectOverride) {
|
||||||
replace({pathname: "/(unauth)"});
|
replace({pathname: "/(unauth)"});
|
||||||
}
|
}
|
||||||
}, [user, ready]);
|
}, [user, ready, redirectOverride]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = Notifications.addNotificationReceivedListener(notification => {
|
const sub = Notifications.addNotificationReceivedListener(notification => {
|
||||||
@ -175,7 +180,8 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{user, profileType, profileData, setProfileData, refreshProfileData}}>
|
<AuthContext.Provider
|
||||||
|
value={{user, profileType, profileData, setProfileData, refreshProfileData, setRedirectOverride}}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
|
import { useCreateNote } from "@/hooks/firebase/useCreateNote";
|
||||||
|
import { useDeleteNote } from "@/hooks/firebase/useDeleteNote";
|
||||||
|
import { useGetNotes } from "@/hooks/firebase/useGetNotes";
|
||||||
|
import { useUpdateNote } from "@/hooks/firebase/useUpdateNote";
|
||||||
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
import { 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;
|
||||||
@ -8,7 +13,7 @@ export interface IBrainDump {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IBrainDumpContext {
|
interface IBrainDumpContext {
|
||||||
brainDumps: IBrainDump[];
|
brainDumps: IBrainDump[] | undefined;
|
||||||
updateBrainDumpItem: (id: number, changes: Partial<IBrainDump>) => void;
|
updateBrainDumpItem: (id: number, changes: Partial<IBrainDump>) => void;
|
||||||
isAddingBrainDump: boolean;
|
isAddingBrainDump: boolean;
|
||||||
setIsAddingBrainDump: (value: boolean) => void;
|
setIsAddingBrainDump: (value: boolean) => void;
|
||||||
@ -23,70 +28,43 @@ const BrainDumpContext = createContext<IBrainDumpContext | undefined>(
|
|||||||
export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
|
export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { data: brainDumps } = useGetNotes();
|
||||||
|
const { mutate: deleteNote } = useDeleteNote();
|
||||||
|
const { mutateAsync: createBrainDump } = useCreateNote();
|
||||||
|
const { mutateAsync: updateNoteMutate } = useUpdateNote();
|
||||||
|
|
||||||
const [isAddingBrainDump, setIsAddingBrainDump] = useState<boolean>(false);
|
const [isAddingBrainDump, setIsAddingBrainDump] = useState<boolean>(false);
|
||||||
const [brainDumps, setBrainDumps] = useState<IBrainDump[]>([
|
|
||||||
{
|
|
||||||
id: 0,
|
|
||||||
title: "Favorite Weekend Activities",
|
|
||||||
description:
|
|
||||||
"What's something fun we can do together this weekend? Maybe a new game, a picnic?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: "What’s For Dinner",
|
|
||||||
description:
|
|
||||||
"What’s one meal you’d love to have for dinner this week?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: "The Best Thing About Today",
|
|
||||||
description:
|
|
||||||
"What was the highlight of your day? Let’s each take a moment to share something!",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: "A Dream Vacation Spot",
|
|
||||||
description:
|
|
||||||
"If we could go anywhere in the world right now, where would it be? Everyone pick one dream destination and tell us why.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
title: "Favorite Childhood Memory",
|
|
||||||
description:
|
|
||||||
"What’s a favorite memory from your childhood? Let’s take a trip down memory lane and share some of the moments that made us smile.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
title: "A New Family Tradition",
|
|
||||||
description:
|
|
||||||
"What’s one new tradition we could start as a family? Maybe a weekly movie night, a monthly game day, or a yearly family trip. Share your ideas!",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const addBrainDump = (BrainDump: IBrainDump) => {
|
const addBrainDump = (BrainDump: IBrainDump) => {
|
||||||
setBrainDumps((prevBrainDumps) => [
|
createBrainDump(BrainDump);
|
||||||
...prevBrainDumps,
|
|
||||||
{
|
|
||||||
...BrainDump,
|
|
||||||
id: prevBrainDumps.length
|
|
||||||
? prevBrainDumps[prevBrainDumps.length - 1].id + 1
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBrainDumpItem = (id: number, changes: Partial<IBrainDump>) => {
|
const updateBrainDumpItem = (id: number, changes: Partial<IBrainDump>) => {
|
||||||
setBrainDumps((prevBrainDumps) =>
|
updateNoteMutate(
|
||||||
prevBrainDumps.map((BrainDump) =>
|
{
|
||||||
BrainDump.id === id ? { ...BrainDump, ...changes } : BrainDump
|
id: id,
|
||||||
)
|
changes: changes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Note updated successfully", data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update note:", error);
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteBrainDump = (id: number) => {
|
const deleteBrainDump = (id: number) => {
|
||||||
setBrainDumps((prevBrainDumps) =>
|
deleteNote(id.toString(), {
|
||||||
prevBrainDumps.filter((BrainDump) => BrainDump.id !== id)
|
onSuccess: () => {
|
||||||
);
|
console.log("Feedback deleted successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to delete feedback:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,7 +75,7 @@ export const BrainDumpProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
isAddingBrainDump,
|
isAddingBrainDump,
|
||||||
setIsAddingBrainDump,
|
setIsAddingBrainDump,
|
||||||
addBrainDump,
|
addBrainDump,
|
||||||
deleteBrainDump
|
deleteBrainDump,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
87
contexts/FeedbackContext.tsx
Normal file
87
contexts/FeedbackContext.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { useCreateFeedback } from "@/hooks/firebase/useCreateFeedback";
|
||||||
|
import { useDeleteFeedback } from "@/hooks/firebase/useDeleteFeedback";
|
||||||
|
import { useGetFeedbacks } from "@/hooks/firebase/useGetFeedbacks";
|
||||||
|
import { useUpdateFeedback } from "@/hooks/firebase/useUpdateFeedback";
|
||||||
|
import { MaterialCommunityIcons } from "@expo/vector-icons";
|
||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
export interface IFeedback {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFeedbackContext {
|
||||||
|
feedbacks: IFeedback[] | undefined;
|
||||||
|
isAddingFeedback: boolean;
|
||||||
|
setIsAddingFeedback: (value: boolean) => void;
|
||||||
|
addFeedback: (BrainDump: IFeedback) => void;
|
||||||
|
updateFeedback: (id: number, changes: Partial<IFeedback>) => void;
|
||||||
|
deleteFeedback: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeedbackContext = createContext<IFeedbackContext | undefined>(undefined);
|
||||||
|
|
||||||
|
export const FeedbackProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
mutateAsync: createFeedback,
|
||||||
|
isLoading: isAdding,
|
||||||
|
isError,
|
||||||
|
} = useCreateFeedback();
|
||||||
|
const { data: feedbacks } = useGetFeedbacks();
|
||||||
|
const { mutate: deleteFeedbackMutate } = useDeleteFeedback();
|
||||||
|
const { mutate: updateFeedbackMutate } = useUpdateFeedback();
|
||||||
|
|
||||||
|
const [isAddingFeedback, setIsAddingFeedback] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const addFeedback = (Feedback: IFeedback) => {
|
||||||
|
createFeedback({ title: Feedback.title, text: Feedback.text });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFeedback = (id: number, changes: Partial<IFeedback>) => {
|
||||||
|
updateFeedbackMutate(
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
changes: changes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
console.log("Feedback updated successfully", data);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to update feedback:", error);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteFeedback = (id: number) => {
|
||||||
|
deleteFeedbackMutate(id.toString(), {
|
||||||
|
onSuccess: () => {
|
||||||
|
console.log("Feedback deleted successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Failed to delete feedback:", error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FeedbackContext.Provider
|
||||||
|
value={{
|
||||||
|
feedbacks,
|
||||||
|
isAddingFeedback,
|
||||||
|
setIsAddingFeedback,
|
||||||
|
addFeedback,
|
||||||
|
updateFeedback,
|
||||||
|
deleteFeedback,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FeedbackContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFeedbackContext = () => useContext(FeedbackContext)!;
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import { createContext, FC, ReactNode, useContext, useState } from "react";
|
import { createContext, FC, ReactNode, useContext, useState } from "react";
|
||||||
import {IToDo} from "@/hooks/firebase/types/todoData";
|
import {IToDo} from "@/hooks/firebase/types/todoData";
|
||||||
import {useGetGroceries} from "@/hooks/firebase/useGetGroceries";
|
|
||||||
import {useGetTodos} from "@/hooks/firebase/useGetTodos";
|
import {useGetTodos} from "@/hooks/firebase/useGetTodos";
|
||||||
import {useCreateGrocery} from "@/hooks/firebase/useCreateGrocery";
|
|
||||||
import {useCreateTodo} from "@/hooks/firebase/useCreateTodo";
|
import {useCreateTodo} from "@/hooks/firebase/useCreateTodo";
|
||||||
import {useUpdateTodo} from "@/hooks/firebase/useUpdateTodo";
|
import {useUpdateTodo} from "@/hooks/firebase/useUpdateTodo";
|
||||||
|
|
||||||
|
|||||||
@ -18,49 +18,50 @@ 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} = eventData;
|
const { familyId, creatorId, email } = eventData;
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
console.log('Event has an email field. Skipping notification.');
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pushTokens.length) {
|
let pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
|
||||||
pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
|
|
||||||
if (!pushTokens.length) {
|
if (!pushTokens.length) {
|
||||||
console.log('No push tokens available for the event.');
|
console.log('No push tokens available for the event.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Increment event count for debouncing
|
|
||||||
eventCount++;
|
eventCount++;
|
||||||
|
|
||||||
if (notificationTimeout) {
|
if (notificationTimeout) {
|
||||||
clearTimeout(notificationTimeout); // Reset the timer if events keep coming
|
clearTimeout(notificationTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a debounce time (e.g., 5 seconds)
|
|
||||||
notificationTimeout = setTimeout(async () => {
|
notificationTimeout = setTimeout(async () => {
|
||||||
const eventMessage = eventCount === 1
|
const eventMessage = eventCount === 1
|
||||||
? `An event "${eventData.title}" has been added. Check it out!`
|
? `An event "${eventData.title}" has been added. Check it out!`
|
||||||
: `${eventCount} new events have been added.`;
|
: `${eventCount} new events have been added.`;
|
||||||
|
|
||||||
let messages = [];
|
let messages = pushTokens.map(pushToken => {
|
||||||
for (let pushToken of pushTokens) {
|
|
||||||
if (!Expo.isExpoPushToken(pushToken)) {
|
if (!Expo.isExpoPushToken(pushToken)) {
|
||||||
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
||||||
continue;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push({
|
return {
|
||||||
to: pushToken,
|
to: pushToken,
|
||||||
sound: 'default',
|
sound: 'default',
|
||||||
title: 'New Events Added!',
|
title: 'New Events Added!',
|
||||||
body: eventMessage,
|
body: eventMessage,
|
||||||
data: {eventId: context.params.eventId},
|
data: { eventId: context.params.eventId },
|
||||||
});
|
};
|
||||||
}
|
}).filter(Boolean);
|
||||||
|
|
||||||
let chunks = expo.chunkPushNotifications(messages);
|
let chunks = expo.chunkPushNotifications(messages);
|
||||||
let tickets = [];
|
let tickets = [];
|
||||||
@ -75,7 +76,7 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
console.log('Notification successfully sent:', ticket.id);
|
console.log('Notification successfully sent:', ticket.id);
|
||||||
} else if (ticket.status === 'error') {
|
} else if (ticket.status === 'error') {
|
||||||
console.error(`Notification error: ${ticket.message}`);
|
console.error(`Notification error: ${ticket.message}`);
|
||||||
if (ticket.details && ticket.details.error === 'DeviceNotRegistered') {
|
if (ticket.details?.error === 'DeviceNotRegistered') {
|
||||||
await removeInvalidPushToken(ticket.to);
|
await removeInvalidPushToken(ticket.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,10 +86,10 @@ exports.sendNotificationOnEventCreation = functions.firestore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eventCount = 0; // Reset the event count after sending notification
|
eventCount = 0;
|
||||||
pushTokens = []; // Reset push tokens for the next round
|
pushTokens = [];
|
||||||
|
|
||||||
}, 5000); // Debounce time (5 seconds)
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ exports.generateCustomToken = onRequest(async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => {
|
exports.refreshTokens = functions.pubsub.schedule('every 1 hours').onRun(async (context) => {
|
||||||
console.log('Running token refresh job...');
|
console.log('Running token refresh job...');
|
||||||
|
|
||||||
const profilesSnapshot = await db.collection('Profiles').get();
|
const profilesSnapshot = await db.collection('Profiles').get();
|
||||||
@ -191,7 +192,7 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
|
|||||||
if (profileData.googleAccounts) {
|
if (profileData.googleAccounts) {
|
||||||
try {
|
try {
|
||||||
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
for (const googleEmail of Object.keys(profileData?.googleAccounts)) {
|
||||||
const googleToken = profileData?.googleAccounts?.[googleEmail];
|
const googleToken = profileData?.googleAccounts?.[googleEmail]?.refreshToken;
|
||||||
if (googleToken) {
|
if (googleToken) {
|
||||||
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
const refreshedGoogleToken = await refreshGoogleToken(googleToken);
|
||||||
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
const updatedGoogleAccounts = {...profileData.googleAccounts, [googleEmail]: refreshedGoogleToken};
|
||||||
@ -238,29 +239,35 @@ exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to refresh Google token
|
async function refreshGoogleToken(refreshToken) {
|
||||||
async function refreshGoogleToken(token) {
|
try {
|
||||||
// Assuming you use OAuth2 token refresh flow
|
|
||||||
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: token, // Add refresh token stored previously
|
refresh_token: refreshToken,
|
||||||
client_id: 'YOUR_GOOGLE_CLIENT_ID',
|
client_id: "406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com", // Web client ID from googleConfig
|
||||||
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data.access_token; // Return new access token
|
return response.data.access_token; // Return the new access token
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Google token:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMicrosoftToken(token) {
|
async function refreshMicrosoftToken(refreshToken) {
|
||||||
|
try {
|
||||||
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: token, // Add refresh token stored previously
|
refresh_token: refreshToken,
|
||||||
client_id: 'YOUR_MICROSOFT_CLIENT_ID',
|
client_id: "13c79071-1066-40a9-9f71-b8c4b138b4af", // Client ID from microsoftConfig
|
||||||
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
|
scope: "openid profile email offline_access Calendars.ReadWrite User.Read", // Scope from microsoftConfig
|
||||||
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data.access_token; // Return new access token
|
return response.data.access_token; // Return the new access token
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refreshing Microsoft token:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPushTokensForEvent() {
|
async function getPushTokensForEvent() {
|
||||||
|
|||||||
2353
firebase/functions/yarn.lock
Normal file
2353
firebase/functions/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,4 +11,22 @@ export interface IToDo {
|
|||||||
creatorId?: string;
|
creatorId?: string;
|
||||||
familyId?: string;
|
familyId?: string;
|
||||||
assignees?: string[]; // Optional list of assignees
|
assignees?: string[]; // Optional list of assignees
|
||||||
|
connectedTodoId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DAYS_OF_WEEK_ENUM = {
|
||||||
|
MONDAY: "Monday",
|
||||||
|
TUESDAY: "Tuesday",
|
||||||
|
WEDNESDAY: "Wednesday",
|
||||||
|
THURSDAY: "Thursday",
|
||||||
|
FRIDAY: "Friday",
|
||||||
|
SATURDAY: "Saturday",
|
||||||
|
SUNDAY: "Sunday"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REPEAT_TYPE = {
|
||||||
|
NONE: "None",
|
||||||
|
EVERY_WEEK: "Every week",
|
||||||
|
ONCE_A_MONTH: "Once a month",
|
||||||
|
ONCE_A_YEAR: "Once a year"
|
||||||
}
|
}
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import storage from "@react-native-firebase/storage";
|
import storage from "@react-native-firebase/storage";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
import { Platform } from "react-native";
|
import {Platform} from "react-native";
|
||||||
|
|
||||||
export const useChangeProfilePicture = () => {
|
export const useChangeProfilePicture = (customUserId?: string) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { user, refreshProfileData } = useAuthContext();
|
const {user, refreshProfileData} = useAuthContext();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["changeProfilePicture"],
|
mutationKey: ["changeProfilePicture"],
|
||||||
@ -38,20 +38,24 @@ export const useChangeProfilePicture = () => {
|
|||||||
const downloadURL = await reference.getDownloadURL();
|
const downloadURL = await reference.getDownloadURL();
|
||||||
console.log("Download URL:", downloadURL);
|
console.log("Download URL:", downloadURL);
|
||||||
|
|
||||||
|
if(!customUserId) {
|
||||||
await firestore()
|
await firestore()
|
||||||
.collection("Profiles")
|
.collection("Profiles")
|
||||||
.doc(user?.uid)
|
.doc(user?.uid)
|
||||||
.update({ pfp: downloadURL });
|
.update({pfp: downloadURL});
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error uploading profile picture:", e.message);
|
console.error("Error uploading profile picture:", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Invalidate queries to refresh profile data
|
// Invalidate queries to refresh profile data
|
||||||
|
if (!customUserId) {
|
||||||
queryClient.invalidateQueries("Profiles");
|
queryClient.invalidateQueries("Profiles");
|
||||||
refreshProfileData();
|
refreshProfileData();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
39
hooks/firebase/useClearTokens.ts
Normal file
39
hooks/firebase/useClearTokens.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {useMutation} from "react-query";
|
||||||
|
import {UserProfile} from "@firebase/auth";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||||
|
|
||||||
|
export const useClearTokens = () => {
|
||||||
|
const {profileData} = useAuthContext();
|
||||||
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["clearTokens"],
|
||||||
|
mutationFn: async ({provider, email}: {
|
||||||
|
provider: "google" | "outlook" | "apple",
|
||||||
|
email: string
|
||||||
|
}) => {
|
||||||
|
const newUserData: Partial<UserProfile> = {};
|
||||||
|
if (provider === "google") {
|
||||||
|
let googleAccounts = profileData?.googleAccounts;
|
||||||
|
if (googleAccounts) {
|
||||||
|
googleAccounts[email] = null;
|
||||||
|
newUserData.googleAccounts = googleAccounts;
|
||||||
|
}
|
||||||
|
} else if (provider === "outlook") {
|
||||||
|
let microsoftAccounts = profileData?.microsoftAccounts;
|
||||||
|
if (microsoftAccounts) {
|
||||||
|
microsoftAccounts[email] = null;
|
||||||
|
newUserData.microsoftAccounts = microsoftAccounts;
|
||||||
|
}
|
||||||
|
} else if (provider === "apple") {
|
||||||
|
let appleAccounts = profileData?.appleAccounts;
|
||||||
|
if (appleAccounts) {
|
||||||
|
appleAccounts[email] = null;
|
||||||
|
newUserData.appleAccounts = appleAccounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await updateUserData({newUserData});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -30,9 +30,10 @@ export const useCreateEvent = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const newDoc = firestore().collection('Events').doc();
|
||||||
await firestore()
|
await firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.add({...eventData, creatorId: currentUser?.uid, familyId: profileData?.familyId});
|
.add({...eventData, id: newDoc.id, creatorId: currentUser?.uid, familyId: profileData?.familyId});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
@ -44,34 +45,41 @@ export const useCreateEvent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCreateEventsFromProvider = () => {
|
export const useCreateEventsFromProvider = () => {
|
||||||
const {user: currentUser} = useAuthContext();
|
const { user: currentUser } = useAuthContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["createEventsFromProvider"],
|
mutationKey: ["createEventsFromProvider"],
|
||||||
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
|
mutationFn: async (eventDataArray: Partial<EventData>[]) => {
|
||||||
try {
|
try {
|
||||||
for (const eventData of eventDataArray) {
|
// Create an array of promises for each event's Firestore read/write operation
|
||||||
|
const promises = eventDataArray.map(async (eventData) => {
|
||||||
console.log("Processing EventData: ", eventData);
|
console.log("Processing EventData: ", eventData);
|
||||||
|
|
||||||
|
// Check if the event already exists
|
||||||
const snapshot = await firestore()
|
const snapshot = await firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.where("id", "==", eventData.id)
|
.where("id", "==", eventData.id)
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
if (snapshot.empty) {
|
if (snapshot.empty) {
|
||||||
await firestore()
|
// Event doesn't exist, so add it
|
||||||
|
return firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.add({...eventData, creatorId: currentUser?.uid});
|
.add({ ...eventData, creatorId: currentUser?.uid });
|
||||||
} else {
|
} else {
|
||||||
console.log("Event already exists, updating...");
|
// Event exists, update it
|
||||||
const docId = snapshot.docs[0].id;
|
const docId = snapshot.docs[0].id;
|
||||||
await firestore()
|
return firestore()
|
||||||
.collection("Events")
|
.collection("Events")
|
||||||
.doc(docId)
|
.doc(docId)
|
||||||
.set({...eventData, creatorId: currentUser?.uid}, {merge: true});
|
.set({ ...eventData, creatorId: currentUser?.uid }, { merge: true });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute all promises in parallel
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error creating/updating events: ", e);
|
console.error("Error creating/updating events: ", e);
|
||||||
}
|
}
|
||||||
|
|||||||
85
hooks/firebase/useCreateFeedback.ts
Normal file
85
hooks/firebase/useCreateFeedback.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
export const useCreateFeedback = () => {
|
||||||
|
const {user: currentUser, profileData} = useAuthContext()
|
||||||
|
const queryClients = useQueryClient()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createFeedback"],
|
||||||
|
mutationFn: async (feedback: Partial<IFeedback>) => {
|
||||||
|
try {
|
||||||
|
if (feedback.id) {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.where("id", "==", feedback.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!snapshot.empty) {
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.doc(docId)
|
||||||
|
.set({
|
||||||
|
...feedback,
|
||||||
|
creatorId: currentUser?.uid,
|
||||||
|
}, {merge: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newDoc = firestore().collection('Feedbacks').doc();
|
||||||
|
await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.add({...feedback, id: newDoc.id, creatorId: currentUser?.uid});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClients.invalidateQueries("feedbacks")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCreateFeedbacksFromProvider = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createFeedbacksFromProvider"],
|
||||||
|
mutationFn: async (feedbackDataArray: Partial<IFeedback>[]) => {
|
||||||
|
try {
|
||||||
|
const promises = feedbackDataArray.map(async (feedbackData) => {
|
||||||
|
console.log("Processing FeedbackData: ", feedbackData);
|
||||||
|
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.where("id", "==", feedbackData.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
return firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.add({ ...feedbackData, creatorId: currentUser?.uid });
|
||||||
|
} else {
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
return firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.doc(docId)
|
||||||
|
.set({ ...feedbackData, creatorId: currentUser?.uid }, { merge: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error creating/updating feedbacks: ", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("feedbacks");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
91
hooks/firebase/useCreateNote.ts
Normal file
91
hooks/firebase/useCreateNote.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
|
|
||||||
|
export const useCreateNote = () => {
|
||||||
|
const { user: currentUser, profileData } = useAuthContext();
|
||||||
|
const queryClients = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createNote"],
|
||||||
|
mutationFn: async (note: Partial<IBrainDump>) => {
|
||||||
|
try {
|
||||||
|
if (note.id) {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.where("id", "==", note.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!snapshot.empty) {
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.doc(docId)
|
||||||
|
.set(
|
||||||
|
{
|
||||||
|
...note,
|
||||||
|
creatorId: currentUser?.uid,
|
||||||
|
},
|
||||||
|
{ merge: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newDoc = firestore().collection("BrainDumps").doc();
|
||||||
|
await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.add({ ...note, id: newDoc.id, creatorId: currentUser?.uid });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClients.invalidateQueries("braindumps");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateNotesFromProvider = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createNotesFromProvider"],
|
||||||
|
mutationFn: async (noteDataArray: Partial<IFeedback>[]) => {
|
||||||
|
try {
|
||||||
|
const promises = noteDataArray.map(async (noteData) => {
|
||||||
|
console.log("Processing NoteData: ", noteData);
|
||||||
|
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.where("id", "==", noteData.id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
return firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.add({ ...noteData, creatorId: currentUser?.uid });
|
||||||
|
} else {
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
return firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.doc(docId)
|
||||||
|
.set(
|
||||||
|
{ ...noteData, creatorId: currentUser?.uid },
|
||||||
|
{ merge: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error creating/updating braindumps: ", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("braindumps");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -15,7 +15,7 @@ export const useCreateSubUser = () => {
|
|||||||
return await functions().httpsCallable("createSubUser")({
|
return await functions().httpsCallable("createSubUser")({
|
||||||
...userProfile,
|
...userProfile,
|
||||||
email,
|
email,
|
||||||
familyId: profileData?.familyId
|
familyId: profileData?.familyId!
|
||||||
}) as HttpsCallableResult<{ userId: string }>
|
}) as HttpsCallableResult<{ userId: string }>
|
||||||
} else {
|
} else {
|
||||||
throw Error("Can't create sub-users as a non-parent.")
|
throw Error("Can't create sub-users as a non-parent.")
|
||||||
|
|||||||
@ -1,7 +1,17 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import {useMutation, useQueryClient} 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 { IToDo } from "@/hooks/firebase/types/todoData";
|
import {DAYS_OF_WEEK_ENUM, IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
|
||||||
|
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";
|
||||||
|
|
||||||
|
export const daysOfWeek = [
|
||||||
|
DAYS_OF_WEEK_ENUM.MONDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.TUESDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.WEDNESDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.THURSDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.FRIDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.SATURDAY,
|
||||||
|
DAYS_OF_WEEK_ENUM.SUNDAY];
|
||||||
|
|
||||||
export const useCreateTodo = () => {
|
export const useCreateTodo = () => {
|
||||||
const { user: currentUser, profileData } = useAuthContext();
|
const { user: currentUser, profileData } = useAuthContext();
|
||||||
@ -11,10 +21,92 @@ export const useCreateTodo = () => {
|
|||||||
mutationKey: ["createTodo"],
|
mutationKey: ["createTodo"],
|
||||||
mutationFn: async (todoData: Partial<IToDo>) => {
|
mutationFn: async (todoData: Partial<IToDo>) => {
|
||||||
try {
|
try {
|
||||||
|
if (todoData.repeatType === REPEAT_TYPE.NONE) {
|
||||||
const newDoc = firestore().collection('Todos').doc();
|
const newDoc = firestore().collection('Todos').doc();
|
||||||
|
let originalTodo = {...todoData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid}
|
||||||
await firestore()
|
await firestore()
|
||||||
.collection("Todos")
|
.collection("Todos")
|
||||||
.add({...todoData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid})
|
.add(originalTodo);
|
||||||
|
} else {
|
||||||
|
// Create the one original to do
|
||||||
|
const newDoc = firestore().collection('Todos').doc();
|
||||||
|
let originalTodo = {...todoData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid, connectedTodoId: newDoc.id};
|
||||||
|
|
||||||
|
originalTodo = resolveTodoAlternatingAssignees(todoData, originalTodo, 0);
|
||||||
|
|
||||||
|
await firestore()
|
||||||
|
.collection("Todos")
|
||||||
|
.add(originalTodo);
|
||||||
|
|
||||||
|
const batch = firestore().batch();
|
||||||
|
|
||||||
|
if (todoData.repeatType === REPEAT_TYPE.EVERY_WEEK) {
|
||||||
|
|
||||||
|
let date = originalTodo.date;
|
||||||
|
let repeatDays = originalTodo.repeatDays;
|
||||||
|
const dates = [];
|
||||||
|
|
||||||
|
const originalDateDay = format(date, 'EEEE');
|
||||||
|
const originalNumber = daysOfWeek.indexOf(originalDateDay);
|
||||||
|
repeatDays?.forEach((day) => {
|
||||||
|
let number = daysOfWeek.indexOf(day);
|
||||||
|
let newDate;
|
||||||
|
if (originalNumber > number) {
|
||||||
|
let diff = originalNumber - number;
|
||||||
|
newDate = subDays(date, diff);
|
||||||
|
} else {
|
||||||
|
let diff = number - originalNumber;
|
||||||
|
newDate = addDays(date, diff);
|
||||||
|
}
|
||||||
|
dates.push(newDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: for the next 52 weeks
|
||||||
|
let index = 1;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
dates?.forEach((dateToAdd) => {
|
||||||
|
index ++;
|
||||||
|
let newTodoDate = addWeeks(dateToAdd, i);
|
||||||
|
if (compareAsc(newTodoDate, originalTodo.date) !== 0) {
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...originalTodo, id: docRef.id, date: newTodoDate, connectedTodoId: newDoc.id };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, index);
|
||||||
|
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (todoData.repeatType === REPEAT_TYPE.ONCE_A_MONTH) {
|
||||||
|
|
||||||
|
// for the next 12 months
|
||||||
|
for (let i = 1; i < 12; i++) {
|
||||||
|
let date = originalTodo.date;
|
||||||
|
const nextMonth = addMonths(date, i);
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...originalTodo, id: docRef.id, date: nextMonth, connectedTodoId: newDoc.id };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, i);
|
||||||
|
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
} else if (todoData.repeatType === REPEAT_TYPE.ONCE_A_YEAR) {
|
||||||
|
|
||||||
|
// for the next 5 years
|
||||||
|
for (let i = 1; i < 5; i++) {
|
||||||
|
let date = originalTodo.date;
|
||||||
|
const nextMonth = addYears(date, i);
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...originalTodo, id: docRef.id, date: nextMonth, connectedTodoId: newDoc.id };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, i);
|
||||||
|
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -24,3 +116,14 @@ export const useCreateTodo = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const resolveTodoAlternatingAssignees = (todoData, newTodo, i) => {
|
||||||
|
if (todoData.assignees && todoData.rotate && todoData?.assignees?.length !== 0) {
|
||||||
|
const assignees = todoData.assignees;
|
||||||
|
const assignee = assignees[i % assignees.length];
|
||||||
|
|
||||||
|
newTodo = {...newTodo, assignees: [assignee]};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newTodo;
|
||||||
|
}
|
||||||
39
hooks/firebase/useDeleteEvent.ts
Normal file
39
hooks/firebase/useDeleteEvent.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
|
export const useDeleteEvent = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["deleteEvent"],
|
||||||
|
mutationFn: async ({eventId, docId}: { eventId?: string; docId?: string }) => {
|
||||||
|
try {
|
||||||
|
if (docId) {
|
||||||
|
await firestore()
|
||||||
|
.collection("Events")
|
||||||
|
.doc(docId)
|
||||||
|
.delete();
|
||||||
|
} else if (eventId) {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Events")
|
||||||
|
.where("id", "==", eventId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const doc = snapshot.docs[0];
|
||||||
|
if (doc) {
|
||||||
|
await doc.ref.delete();
|
||||||
|
} else {
|
||||||
|
console.warn("Event not found");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("No identifier provided");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("events");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
45
hooks/firebase/useDeleteFeedback.ts
Normal file
45
hooks/firebase/useDeleteFeedback.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
|
export const useDeleteFeedback = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["deleteFeedback"],
|
||||||
|
mutationFn: async (feedbackId: string) => {
|
||||||
|
try {
|
||||||
|
// Find the document with matching id field
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.where("id", "==", feedbackId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
throw new Error("Feedback not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first matching document
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
|
||||||
|
// Optional: Check if the current user is the creator
|
||||||
|
const feedbackData = snapshot.docs[0].data();
|
||||||
|
if (feedbackData.creatorId !== currentUser?.uid) {
|
||||||
|
throw new Error(
|
||||||
|
"Unauthorized: You can only delete your own feedback"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the document
|
||||||
|
await firestore().collection("Feedbacks").doc(docId).delete();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error deleting feedback: ", e);
|
||||||
|
throw e; // Re-throw the error to be handled by the mutation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("feedbacks");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
39
hooks/firebase/useDeleteNote.ts
Normal file
39
hooks/firebase/useDeleteNote.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
|
||||||
|
export const useDeleteNote = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["deleteNote"],
|
||||||
|
mutationFn: async (noteId: string) => {
|
||||||
|
try {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.where("id", "==", noteId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
throw new Error("Note not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
|
||||||
|
const noteData = snapshot.docs[0].data();
|
||||||
|
if (noteData.creatorId !== currentUser?.uid) {
|
||||||
|
throw new Error("Unauthorized: You can only delete your own Note");
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore().collection("BrainDumps").doc(docId).delete();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error deleting note: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries("braindumps");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,23 +1,24 @@
|
|||||||
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";
|
||||||
|
|
||||||
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({
|
||||||
queryKey: ["events", user?.uid, isFamilyView],
|
queryKey: ["events", user?.uid, isFamilyView],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const db = firestore();
|
const db = firestore();
|
||||||
|
|
||||||
const userId = user?.uid;
|
const userId = user?.uid;
|
||||||
const familyId = profileData?.familyId;
|
const familyId = profileData?.familyId;
|
||||||
|
|
||||||
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);
|
const familyQuery = db.collection("Events").where("familyID", "==", familyId);
|
||||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
||||||
@ -29,12 +30,14 @@ export const useGetEvents = () => {
|
|||||||
attendeeQuery.get(),
|
attendeeQuery.get(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Collect all events
|
||||||
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
const familyEvents = familySnapshot.docs.map(doc => doc.data());
|
||||||
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
|
const creatorEvents = creatorSnapshot.docs.map(doc => doc.data());
|
||||||
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
const attendeeEvents = attendeeSnapshot.docs.map(doc => doc.data());
|
||||||
|
|
||||||
allEvents = [...familyEvents, ...creatorEvents, ...attendeeEvents];
|
allEvents = [...familyEvents, ...creatorEvents, ...attendeeEvents];
|
||||||
} else {
|
} else {
|
||||||
|
// Only include creator and attendee events when family view is off
|
||||||
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
const creatorQuery = db.collection("Events").where("creatorId", "==", userId);
|
||||||
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
const attendeeQuery = db.collection("Events").where("attendees", "array-contains", userId);
|
||||||
|
|
||||||
@ -49,19 +52,28 @@ export const useGetEvents = () => {
|
|||||||
allEvents = [...creatorEvents, ...attendeeEvents];
|
allEvents = [...creatorEvents, ...attendeeEvents];
|
||||||
}
|
}
|
||||||
|
|
||||||
allEvents = allEvents.filter((event, index, self) =>
|
// Use a Map to ensure uniqueness only for events with IDs
|
||||||
index === self.findIndex(e => e.id === event.id)
|
const uniqueEventsMap = new Map();
|
||||||
);
|
allEvents.forEach(event => {
|
||||||
|
if (event.id) {
|
||||||
|
uniqueEventsMap.set(event.id, event); // Ensure uniqueness for events with IDs
|
||||||
|
} else {
|
||||||
|
uniqueEventsMap.set(Math.random().toString(36), event); // Generate a temp key for events without ID
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const uniqueEvents = Array.from(uniqueEventsMap.values());
|
||||||
|
|
||||||
allEvents = allEvents.filter(event => {
|
// Filter out private events unless the user is the creator
|
||||||
|
const filteredEvents = uniqueEvents.filter(event => {
|
||||||
if (event.private) {
|
if (event.private) {
|
||||||
return event.creatorId === userId;
|
return event.creatorId === userId;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attach event colors and return the final list of events
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
allEvents.map(async (event) => {
|
filteredEvents.map(async (event) => {
|
||||||
const profileSnapshot = await db
|
const profileSnapshot = await db
|
||||||
.collection("Profiles")
|
.collection("Profiles")
|
||||||
.doc(event.creatorId)
|
.doc(event.creatorId)
|
||||||
@ -71,13 +83,13 @@ export const useGetEvents = () => {
|
|||||||
const eventColor = profileData?.eventColor || colorMap.pink;
|
const eventColor = profileData?.eventColor || colorMap.pink;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id || Math.random().toString(36).substr(2, 9), // Generate temp ID if missing
|
||||||
title: event.title,
|
title: event.title,
|
||||||
start: new Date(event.startDate.seconds * 1000),
|
start: new Date(event.startDate.seconds * 1000),
|
||||||
end: new Date(event.endDate.seconds * 1000),
|
end: new Date(event.endDate.seconds * 1000),
|
||||||
hideHours: event.allDay,
|
hideHours: event.allDay,
|
||||||
eventColor: eventColor,
|
eventColor,
|
||||||
notes: event.notes
|
notes: event.notes,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
28
hooks/firebase/useGetFeedbacks.ts
Normal file
28
hooks/firebase/useGetFeedbacks.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
export const useGetFeedbacks = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
|
||||||
|
return useQuery<IFeedback[]>({
|
||||||
|
queryKey: ["feedbacks", currentUser?.uid],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.where("creatorId", "==", currentUser?.uid)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => ({
|
||||||
|
...doc.data(),
|
||||||
|
})) as IFeedback[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching feedbacks:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: !!currentUser?.uid, // Only run query if we have a user ID
|
||||||
|
});
|
||||||
|
};
|
||||||
28
hooks/firebase/useGetNotes.ts
Normal file
28
hooks/firebase/useGetNotes.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
|
|
||||||
|
export const useGetNotes = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
|
||||||
|
return useQuery<IBrainDump[]>({
|
||||||
|
queryKey: ["braindumps", currentUser?.uid],
|
||||||
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.where("creatorId", "==", currentUser?.uid)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs.map((doc) => ({
|
||||||
|
...doc.data(),
|
||||||
|
})) as IBrainDump[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching braindumps:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: !!currentUser?.uid,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,20 +1,25 @@
|
|||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore, {or, query, where} from "@react-native-firebase/firestore";
|
||||||
import { useAuthContext } from "@/contexts/AuthContext";
|
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
|
||||||
import {UserProfile} from "@/hooks/firebase/types/profileTypes";
|
import { IToDo } from "@/hooks/firebase/types/todoData";
|
||||||
import {IToDo} from "@/hooks/firebase/types/todoData";
|
|
||||||
|
|
||||||
export const useGetTodos = () => {
|
export const useGetTodos = () => {
|
||||||
const { user, profileData } = useAuthContext();
|
const { user, profileData } = useAuthContext();
|
||||||
//TODO: Add role based filtering for todos
|
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["todos", user?.uid],
|
queryKey: ["todos", user?.uid],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const snapshot = await firestore()
|
|
||||||
|
let snapshot;
|
||||||
|
if (profileData?.userType === ProfileType.PARENT) {
|
||||||
|
snapshot = await firestore()
|
||||||
.collection("Todos")
|
.collection("Todos")
|
||||||
.where("familyId", "==", profileData?.familyId)
|
.where("familyId", "==", profileData?.familyId)
|
||||||
.get();
|
.get();
|
||||||
|
} else {
|
||||||
|
let todosQuery = query(firestore().collection("Todos"), or(where("assignees", "array-contains", user?.uid), where("creatorId", "==", user?.uid)));
|
||||||
|
snapshot = await todosQuery.get();
|
||||||
|
}
|
||||||
|
|
||||||
return snapshot.docs.map((doc) => {
|
return snapshot.docs.map((doc) => {
|
||||||
const data = doc.data();
|
const data = doc.data();
|
||||||
@ -23,6 +28,7 @@ export const useGetTodos = () => {
|
|||||||
...data,
|
...data,
|
||||||
id: doc.id,
|
id: doc.id,
|
||||||
date: data.date ? new Date(data.date.seconds * 1000) : null,
|
date: data.date ? new Date(data.date.seconds * 1000) : null,
|
||||||
|
repeatDays: data.repeatDays ?? []
|
||||||
};
|
};
|
||||||
}) as IToDo[];
|
}) as IToDo[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
import functions, {FirebaseFunctionsTypes} from '@react-native-firebase/functions';
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export const useLoginWithQrCode = () => {
|
export const useLoginWithQrCode = () => {
|
||||||
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["loginWithQrCode"],
|
mutationKey: ["loginWithQrCode"],
|
||||||
mutationFn: async ({userId}: { userId: string }) => {
|
mutationFn: async ({userId}: { userId: string }) => {
|
||||||
try {
|
try {
|
||||||
|
setRedirectOverride(true)
|
||||||
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
const res = await functions().httpsCallable("generateCustomToken")({userId}) as FirebaseFunctionsTypes.HttpsCallableResult<{
|
||||||
token: string
|
token: string
|
||||||
}>
|
}>
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { useMutation } from "react-query";
|
import {useMutation} from "react-query";
|
||||||
import auth from "@react-native-firebase/auth";
|
import auth from "@react-native-firebase/auth";
|
||||||
import { ProfileType } from "@/contexts/AuthContext";
|
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
|
||||||
import { useSetUserData } from "./useSetUserData";
|
import {useSetUserData} from "./useSetUserData";
|
||||||
import {uuidv4} from "@firebase/util";
|
import {uuidv4} from "@firebase/util";
|
||||||
import * as Localization from "expo-localization";
|
import * as Localization from "expo-localization";
|
||||||
|
|
||||||
export const useSignUp = () => {
|
export const useSignUp = () => {
|
||||||
const { mutateAsync: setUserData } = useSetUserData();
|
const {setRedirectOverride} = useAuthContext()
|
||||||
|
const {mutateAsync: setUserData} = useSetUserData();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["signUp"],
|
mutationKey: ["signUp"],
|
||||||
@ -21,6 +22,8 @@ export const useSignUp = () => {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
setRedirectOverride(true)
|
||||||
|
|
||||||
await auth()
|
await auth()
|
||||||
.createUserWithEmailAndPassword(email, password)
|
.createUserWithEmailAndPassword(email, password)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|||||||
65
hooks/firebase/useUpdateFeedback.ts
Normal file
65
hooks/firebase/useUpdateFeedback.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IFeedback } from "@/contexts/FeedbackContext";
|
||||||
|
|
||||||
|
interface UpdateFeedbackParams {
|
||||||
|
id: number;
|
||||||
|
changes: Partial<IFeedback>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateFeedback = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateFeedback"],
|
||||||
|
mutationFn: async ({ id, changes }: UpdateFeedbackParams) => {
|
||||||
|
try {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.where("id", "==", id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
throw new Error("Feedback not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
const feedbackData = snapshot.docs[0].data();
|
||||||
|
|
||||||
|
if (feedbackData.creatorId !== currentUser?.uid) {
|
||||||
|
throw new Error(
|
||||||
|
"Unauthorized: You can only update your own feedback"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore()
|
||||||
|
.collection("Feedbacks")
|
||||||
|
.doc(docId)
|
||||||
|
.update({
|
||||||
|
...changes,
|
||||||
|
updatedAt: firestore.FieldValue.serverTimestamp(),
|
||||||
|
lastModifiedBy: currentUser?.uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...feedbackData,
|
||||||
|
...changes,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error updating feedback: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (updatedFeedback) => {
|
||||||
|
queryClient.invalidateQueries("feedbacks");
|
||||||
|
|
||||||
|
queryClient.setQueryData(
|
||||||
|
["feedback", updatedFeedback.id],
|
||||||
|
updatedFeedback
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
65
hooks/firebase/useUpdateNote.ts
Normal file
65
hooks/firebase/useUpdateNote.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useAuthContext } from "@/contexts/AuthContext";
|
||||||
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
|
import firestore from "@react-native-firebase/firestore";
|
||||||
|
import { IBrainDump } from "@/contexts/DumpContext";
|
||||||
|
|
||||||
|
interface UpdateNoteParams {
|
||||||
|
id: number;
|
||||||
|
changes: Partial<IBrainDump>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateNote = () => {
|
||||||
|
const { user: currentUser } = useAuthContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateNote"],
|
||||||
|
mutationFn: async ({ id, changes }: UpdateNoteParams) => {
|
||||||
|
try {
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.where("id", "==", id)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (snapshot.empty) {
|
||||||
|
throw new Error("Note not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const docId = snapshot.docs[0].id;
|
||||||
|
const noteData = snapshot.docs[0].data();
|
||||||
|
|
||||||
|
if (noteData.creatorId !== currentUser?.uid) {
|
||||||
|
throw new Error(
|
||||||
|
"Unauthorized: You can only update your own note"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore()
|
||||||
|
.collection("BrainDumps")
|
||||||
|
.doc(docId)
|
||||||
|
.update({
|
||||||
|
...changes,
|
||||||
|
updatedAt: firestore.FieldValue.serverTimestamp(),
|
||||||
|
lastModifiedBy: currentUser?.uid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...noteData,
|
||||||
|
...changes,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error updating note: ", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (updatedNote) => {
|
||||||
|
queryClient.invalidateQueries("braindumps");
|
||||||
|
|
||||||
|
queryClient.setQueryData(
|
||||||
|
["feedback", updatedNote.id],
|
||||||
|
updatedNote
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,18 +1,153 @@
|
|||||||
import { useMutation, useQueryClient } from "react-query";
|
import { useMutation, useQueryClient } from "react-query";
|
||||||
import firestore from "@react-native-firebase/firestore";
|
import firestore from "@react-native-firebase/firestore";
|
||||||
import { IToDo } from "@/hooks/firebase/types/todoData";
|
import {IToDo, REPEAT_TYPE} from "@/hooks/firebase/types/todoData";
|
||||||
|
import {addDays, addMonths, addWeeks, addYears, compareAsc, format, subDays} from "date-fns";
|
||||||
|
import {daysOfWeek, resolveTodoAlternatingAssignees} from "@/hooks/firebase/useCreateTodo";
|
||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
|
||||||
export const useUpdateTodo = () => {
|
export const useUpdateTodo = () => {
|
||||||
|
const { user: currentUser, profileData } = useAuthContext();
|
||||||
const queryClients = useQueryClient()
|
const queryClients = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["updateTodo"],
|
mutationKey: ["updateTodo"],
|
||||||
mutationFn: async (todoData: Partial<IToDo>) => {
|
mutationFn: async (todoData: Partial<IToDo>) => {
|
||||||
try {
|
try {
|
||||||
|
if (todoData.connectedTodoId) {
|
||||||
|
console.log("CONNECTED")
|
||||||
|
const snapshot = await firestore()
|
||||||
|
.collection("Todos")
|
||||||
|
.where("connectedTodoId", "==", todoData.connectedTodoId)
|
||||||
|
.get();
|
||||||
|
const connectedTodos = snapshot.docs.map((doc) => {
|
||||||
|
const data = doc.data();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
id: doc.id,
|
||||||
|
date: data.date ? new Date(data.date.seconds * 1000) : null,
|
||||||
|
ref: doc.ref
|
||||||
|
};
|
||||||
|
}) as IToDo[];
|
||||||
|
|
||||||
|
console.log("CONNECTED TODO");
|
||||||
|
let filteredTodos = connectedTodos?.filter((item) => compareAsc(format(item.date, 'yyyy-MM-dd'), format(todoData.date, 'yyyy-MM-dd')) === 1 ||
|
||||||
|
compareAsc(format(item.date, 'yyyy-MM-dd'), format(todoData.date, 'yyyy-MM-dd')) === 0).sort((a,b) =>{
|
||||||
|
return b.date?.getSeconds() - a.date?.getSeconds();
|
||||||
|
});
|
||||||
|
|
||||||
|
let firstTodo = filteredTodos?.[0];
|
||||||
|
const batch = firestore().batch();
|
||||||
|
if (compareAsc(format(firstTodo?.date, 'yyyy-MM-dd'), format(todoData.date, 'yyyy-MM-dd')) !== 0 || firstTodo?.repeatType !== todoData.repeatType) {
|
||||||
|
|
||||||
|
console.log("DELETE");
|
||||||
|
filteredTodos?.forEach((item) => {
|
||||||
|
batch.delete(item.ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (todoData.repeatType === REPEAT_TYPE.NONE) {
|
||||||
|
|
||||||
|
console.log("NONE");
|
||||||
|
const newDoc = firestore().collection('Todos').doc();
|
||||||
|
let originalTodo = {...todoData, id: newDoc.id, familyId: profileData?.familyId, creatorId: currentUser?.uid}
|
||||||
|
batch.set(newDoc, originalTodo);
|
||||||
|
} else if (todoData.repeatType === REPEAT_TYPE.EVERY_WEEK) {
|
||||||
|
|
||||||
|
console.log("EVERY WEEK");
|
||||||
|
let date = todoData?.date;
|
||||||
|
let repeatDays = todoData?.repeatDays;
|
||||||
|
const dates = [];
|
||||||
|
|
||||||
|
const originalDateDay = format(date, 'EEEE');
|
||||||
|
const originalNumber = daysOfWeek.indexOf(originalDateDay);
|
||||||
|
repeatDays?.forEach((day) => {
|
||||||
|
let number = daysOfWeek.indexOf(day);
|
||||||
|
let newDate;
|
||||||
|
if (originalNumber > number) {
|
||||||
|
let diff = originalNumber - number;
|
||||||
|
newDate = subDays(date, diff);
|
||||||
|
} else {
|
||||||
|
let diff = number - originalNumber;
|
||||||
|
newDate = addDays(date, diff);
|
||||||
|
}
|
||||||
|
dates.push(newDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
let todosToAddCycles = 4;
|
||||||
|
if (firstTodo?.repeatType === REPEAT_TYPE.EVERY_WEEK) {
|
||||||
|
todosToAddCycles = filteredTodos?.length / firstTodo?.repeatDays?.length;
|
||||||
|
}
|
||||||
|
console.log(todosToAddCycles);
|
||||||
|
let newDoc = firestore().collection("Todos").doc();
|
||||||
|
let originalTodo = { ...todoData, id: newDoc.id, date: todoData.date, connectedTodoId: newDoc?.id };
|
||||||
|
originalTodo = resolveTodoAlternatingAssignees(todoData, originalTodo, 0);
|
||||||
|
batch.set(newDoc, originalTodo);
|
||||||
|
|
||||||
|
console.log(dates);
|
||||||
|
let index = 1;
|
||||||
|
for (let i = 0; i <= todosToAddCycles; i++) {
|
||||||
|
dates?.forEach((dateToAdd) => {
|
||||||
|
index++;
|
||||||
|
let newTodoDate = addWeeks(dateToAdd, i);
|
||||||
|
if (compareAsc(newTodoDate, originalTodo.date) !== 0) {
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...todoData, id: docRef.id, date: newTodoDate, connectedTodoId: newDoc?.id };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, index);
|
||||||
|
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (todoData.repeatType === REPEAT_TYPE.ONCE_A_MONTH) {
|
||||||
|
|
||||||
|
console.log("ONCE A MONTH");
|
||||||
|
// for the next 12 months
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
let date = todoData?.date;
|
||||||
|
const nextMonth = addMonths(date, i);
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...todoData, id: docRef.id, date: nextMonth, connectedTodoId: firstTodo?.connectedTodoId };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, i);
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
} else if (todoData.repeatType === REPEAT_TYPE.ONCE_A_YEAR) {
|
||||||
|
|
||||||
|
console.log("ONCE A YEAR");
|
||||||
|
// for the next 5 years
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let date = todoData?.date;
|
||||||
|
const nextMonth = addYears(date, i);
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
let newTodo = { ...todoData, id: docRef.id, date: nextMonth, connectedTodoId: firstTodo?.connectedTodoId };
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, i);
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
} else if (firstTodo?.repeatDays !== todoData.repeatDays) {
|
||||||
|
|
||||||
|
console.log("UPDATE REPEAT DAYS");
|
||||||
|
await updateRepeatDaysTodos(batch, todoData, firstTodo, filteredTodos)
|
||||||
|
} else {
|
||||||
|
filteredTodos?.forEach((item) => {
|
||||||
|
|
||||||
|
console.log("UPDATE");
|
||||||
|
batch.update(item.ref, {...todoData, date: item.date});
|
||||||
|
})
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("REGULAR UPDATE");
|
||||||
|
console.log(todoData);
|
||||||
await firestore()
|
await firestore()
|
||||||
.collection("Todos")
|
.collection("Todos")
|
||||||
.doc(todoData.id)
|
.doc(todoData.id)
|
||||||
.update(todoData);
|
.update(todoData);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
@ -22,3 +157,61 @@ export const useUpdateTodo = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateRepeatDaysTodos = async (batch: any, todoData: IToDo, firstTodo: IToDo, filteredTodos: IToDo[]) => {
|
||||||
|
const todosToAddCycles = filteredTodos?.length / firstTodo?.repeatDays?.length;
|
||||||
|
console.log(todosToAddCycles);
|
||||||
|
|
||||||
|
filteredTodos?.forEach((item) => {
|
||||||
|
|
||||||
|
batch.update(item.ref, {...todoData, date: item.date});
|
||||||
|
})
|
||||||
|
|
||||||
|
let newRepeatDays = todoData.repeatDays?.filter((element) => firstTodo?.repeatDays?.indexOf(element) === -1);
|
||||||
|
let removeRepeatDays = firstTodo?.repeatDays?.filter((element) => todoData?.repeatDays?.indexOf(element) === -1);
|
||||||
|
const dates = [];
|
||||||
|
|
||||||
|
let date = firstTodo?.date;
|
||||||
|
const originalDateDay = format(date, 'EEEE');
|
||||||
|
const originalNumber = daysOfWeek.indexOf(originalDateDay);
|
||||||
|
newRepeatDays?.forEach((day) => {
|
||||||
|
let number = daysOfWeek.indexOf(day);
|
||||||
|
let newDate;
|
||||||
|
if (originalNumber > number) {
|
||||||
|
let diff = originalNumber - number;
|
||||||
|
newDate = subDays(date, diff);
|
||||||
|
} else {
|
||||||
|
let diff = number - originalNumber;
|
||||||
|
newDate = addDays(date, diff);
|
||||||
|
}
|
||||||
|
dates.push(newDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
for (let i = 0; i < todosToAddCycles; i++) {
|
||||||
|
dates?.forEach((dateToAdd) => {
|
||||||
|
index ++;
|
||||||
|
let newTodoDate = addWeeks(dateToAdd, i);
|
||||||
|
if (compareAsc(newTodoDate, firstTodo?.date) !== 0) {
|
||||||
|
let newTodo = {...todoData, date: newTodoDate};
|
||||||
|
newTodo = resolveTodoAlternatingAssignees(todoData, newTodo, index);
|
||||||
|
|
||||||
|
let docRef = firestore().collection("Todos").doc();
|
||||||
|
batch.set(docRef, newTodo);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRepeatDays?.forEach((removeDay) => {
|
||||||
|
filteredTodos?.forEach((item) => {
|
||||||
|
|
||||||
|
let todoDate = item.date;
|
||||||
|
const todoDateDay = format(todoDate, 'EEEE');
|
||||||
|
|
||||||
|
if (todoDateDay === removeDay) {
|
||||||
|
batch.delete(item.ref);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await batch.commit();
|
||||||
|
}
|
||||||
288
hooks/useCalSync.ts
Normal file
288
hooks/useCalSync.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
|
import {useEffect} from "react";
|
||||||
|
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||||
|
import {useFetchAndSaveGoogleEvents} from "@/hooks/useFetchAndSaveGoogleEvents";
|
||||||
|
import {useFetchAndSaveOutlookEvents} from "@/hooks/useFetchAndSaveOutlookEvents";
|
||||||
|
import {useFetchAndSaveAppleEvents} from "@/hooks/useFetchAndSaveAppleEvents";
|
||||||
|
import * as WebBrowser from "expo-web-browser";
|
||||||
|
import * as Google from "expo-auth-session/providers/google";
|
||||||
|
import * as AuthSession from "expo-auth-session";
|
||||||
|
import * as AppleAuthentication from "expo-apple-authentication";
|
||||||
|
|
||||||
|
const googleConfig = {
|
||||||
|
androidClientId:
|
||||||
|
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
iosClientId:
|
||||||
|
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
webClientId:
|
||||||
|
"406146460310-2u67ab2nbhu23trp8auho1fq4om29fc0.apps.googleusercontent.com",
|
||||||
|
scopes: [
|
||||||
|
"email",
|
||||||
|
"profile",
|
||||||
|
"https://www.googleapis.com/auth/calendar.events.owned",
|
||||||
|
],
|
||||||
|
extraParams: {
|
||||||
|
access_type: "offline",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const microsoftConfig = {
|
||||||
|
clientId: "13c79071-1066-40a9-9f71-b8c4b138b4af",
|
||||||
|
redirectUri: AuthSession.makeRedirectUri({path: "settings"}),
|
||||||
|
scopes: [
|
||||||
|
"openid",
|
||||||
|
"profile",
|
||||||
|
"email",
|
||||||
|
"offline_access",
|
||||||
|
"Calendars.ReadWrite",
|
||||||
|
"User.Read",
|
||||||
|
],
|
||||||
|
authorizationEndpoint:
|
||||||
|
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||||
|
tokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCalSync = () => {
|
||||||
|
const {profileData} = useAuthContext();
|
||||||
|
|
||||||
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
|
const {mutateAsync: fetchAndSaveGoogleEvents, isLoading: isSyncingGoogle} =
|
||||||
|
useFetchAndSaveGoogleEvents();
|
||||||
|
const {
|
||||||
|
mutateAsync: fetchAndSaveOutlookEvents,
|
||||||
|
isLoading: isSyncingOutlook,
|
||||||
|
} = useFetchAndSaveOutlookEvents();
|
||||||
|
const {mutateAsync: fetchAndSaveAppleEvents, isLoading: isSyncingApple} =
|
||||||
|
useFetchAndSaveAppleEvents();
|
||||||
|
|
||||||
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
|
const [_, response, promptAsync] = Google.useAuthRequest(googleConfig);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
signInWithGoogle();
|
||||||
|
}, [response]);
|
||||||
|
|
||||||
|
const signInWithGoogle = async () => {
|
||||||
|
try {
|
||||||
|
if (response?.type === "success") {
|
||||||
|
const {accessToken, refreshToken} = response?.authentication!;
|
||||||
|
|
||||||
|
const userInfoResponse = await fetch(
|
||||||
|
"https://www.googleapis.com/oauth2/v3/userinfo",
|
||||||
|
{
|
||||||
|
headers: {Authorization: `Bearer ${accessToken}`},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const userInfo = await userInfoResponse.json();
|
||||||
|
const googleMail = userInfo.email;
|
||||||
|
|
||||||
|
let googleAccounts = profileData?.googleAccounts || {};
|
||||||
|
const updatedGoogleAccounts = {
|
||||||
|
...googleAccounts,
|
||||||
|
[googleMail]: {accessToken, refreshToken},
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateUserData({
|
||||||
|
newUserData: {googleAccounts: updatedGoogleAccounts},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndSaveGoogleEvents({
|
||||||
|
token: accessToken,
|
||||||
|
email: googleMail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Google sign-in:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMicrosoftSignIn = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Starting Microsoft sign-in...");
|
||||||
|
|
||||||
|
const authRequest = new AuthSession.AuthRequest({
|
||||||
|
clientId: microsoftConfig.clientId,
|
||||||
|
scopes: microsoftConfig.scopes,
|
||||||
|
redirectUri: microsoftConfig.redirectUri,
|
||||||
|
responseType: AuthSession.ResponseType.Code,
|
||||||
|
usePKCE: true, // Enable PKCE
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Auth request created:", authRequest);
|
||||||
|
|
||||||
|
const authResult = await authRequest.promptAsync({
|
||||||
|
authorizationEndpoint: microsoftConfig.authorizationEndpoint,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Auth result:", authResult);
|
||||||
|
|
||||||
|
if (authResult.type === "success" && authResult.params?.code) {
|
||||||
|
const code = authResult.params.code;
|
||||||
|
console.log("Authorization code received:", code);
|
||||||
|
|
||||||
|
// Exchange authorization code for tokens
|
||||||
|
const tokenResponse = await fetch(microsoftConfig.tokenEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: `client_id=${
|
||||||
|
microsoftConfig.clientId
|
||||||
|
}&redirect_uri=${encodeURIComponent(
|
||||||
|
microsoftConfig.redirectUri
|
||||||
|
)}&grant_type=authorization_code&code=${code}&code_verifier=${
|
||||||
|
authRequest.codeVerifier
|
||||||
|
}&scope=${encodeURIComponent(
|
||||||
|
"https://graph.microsoft.com/Calendars.ReadWrite offline_access User.Read"
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Token response status:", tokenResponse.status);
|
||||||
|
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
const errorText = await tokenResponse.text();
|
||||||
|
console.error("Token exchange failed:", errorText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await tokenResponse.json();
|
||||||
|
console.log("Token data received:", tokenData);
|
||||||
|
|
||||||
|
if (tokenData?.access_token) {
|
||||||
|
console.log("Access token received, fetching user info...");
|
||||||
|
|
||||||
|
// Fetch user info from Microsoft Graph API to get the email
|
||||||
|
const userInfoResponse = await fetch(
|
||||||
|
"https://graph.microsoft.com/v1.0/me",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${tokenData.access_token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const userInfo = await userInfoResponse.json();
|
||||||
|
console.log("User info received:", userInfo);
|
||||||
|
|
||||||
|
if (userInfo.error) {
|
||||||
|
console.error("Error fetching user info:", userInfo.error);
|
||||||
|
} else {
|
||||||
|
const outlookMail = userInfo.mail || userInfo.userPrincipalName;
|
||||||
|
|
||||||
|
let microsoftAccounts = profileData?.microsoftAccounts;
|
||||||
|
const updatedMicrosoftAccounts = microsoftAccounts
|
||||||
|
? {...microsoftAccounts, [outlookMail]: tokenData.access_token}
|
||||||
|
: {[outlookMail]: tokenData.access_token};
|
||||||
|
|
||||||
|
await updateUserData({
|
||||||
|
newUserData: {microsoftAccounts: updatedMicrosoftAccounts},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchAndSaveOutlookEvents(
|
||||||
|
tokenData.access_token,
|
||||||
|
outlookMail
|
||||||
|
);
|
||||||
|
console.log("User data updated successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Authentication was not successful:", authResult);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Microsoft sign-in:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppleSignIn = async () => {
|
||||||
|
try {
|
||||||
|
console.log("Starting Apple Sign-in...");
|
||||||
|
|
||||||
|
const credential = await AppleAuthentication.signInAsync({
|
||||||
|
requestedScopes: [
|
||||||
|
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
||||||
|
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Apple sign-in result:", credential);
|
||||||
|
|
||||||
|
alert(JSON.stringify(credential))
|
||||||
|
|
||||||
|
const appleToken = credential.identityToken;
|
||||||
|
const appleMail = credential.email!;
|
||||||
|
|
||||||
|
|
||||||
|
if (appleToken) {
|
||||||
|
console.log("Apple ID token received. Fetch user info if needed...");
|
||||||
|
|
||||||
|
let appleAcounts = profileData?.appleAccounts;
|
||||||
|
const updatedAppleAccounts = appleAcounts
|
||||||
|
? {...appleAcounts, [appleMail]: appleToken}
|
||||||
|
: {[appleMail]: appleToken};
|
||||||
|
|
||||||
|
await updateUserData({
|
||||||
|
newUserData: {appleAccounts: updatedAppleAccounts},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("User data updated with Apple ID token.");
|
||||||
|
await fetchAndSaveAppleEvents({token: appleToken, email: appleMail!});
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Apple authentication was not successful or email was hidden."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Apple Sign-in:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
let isConnectedToGoogle = false;
|
||||||
|
if (profileData?.googleAccounts) {
|
||||||
|
Object.values(profileData?.googleAccounts).forEach((item) => {
|
||||||
|
if (item !== null) {
|
||||||
|
isConnectedToGoogle = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let isConnectedToMicrosoft = false;
|
||||||
|
const microsoftAccounts = profileData?.microsoftAccounts;
|
||||||
|
if (microsoftAccounts) {
|
||||||
|
Object.values(profileData?.microsoftAccounts).forEach((item) => {
|
||||||
|
if (item !== null) {
|
||||||
|
isConnectedToMicrosoft = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let isConnectedToApple = false;
|
||||||
|
if (profileData?.appleAccounts) {
|
||||||
|
Object.values(profileData?.appleAccounts).forEach((item) => {
|
||||||
|
if (item !== null) {
|
||||||
|
isConnectedToApple = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleAppleSignIn,
|
||||||
|
handleMicrosoftSignIn,
|
||||||
|
handleGoogleSignIn: signInWithGoogle,
|
||||||
|
handleStartGoogleSignIn: promptAsync,
|
||||||
|
fetchAndSaveOutlookEvents,
|
||||||
|
fetchAndSaveAppleEvents,
|
||||||
|
fetchAndSaveGoogleEvents,
|
||||||
|
isConnectedToApple,
|
||||||
|
isConnectedToMicrosoft,
|
||||||
|
isConnectedToGoogle,
|
||||||
|
isSyncingOutlook,
|
||||||
|
isSyncingGoogle,
|
||||||
|
isSyncingApple
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||||
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
|
import {fetchiPhoneCalendarEvents} from "@/calendar-integration/apple-calendar-utils";
|
||||||
|
|
||||||
export const useFetchAndSaveAppleEvents = () => {
|
export const useFetchAndSaveAppleEvents = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
|
||||||
@ -29,5 +30,8 @@ export const useFetchAndSaveAppleEvents = () => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["events"])
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
|
import {fetchGoogleCalendarEvents} from "@/calendar-integration/google-calendar-utils";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||||
|
import {useClearTokens} from "@/hooks/firebase/useClearTokens";
|
||||||
|
|
||||||
export const useFetchAndSaveGoogleEvents = () => {
|
export const useFetchAndSaveGoogleEvents = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
const {mutateAsync: clearToken} = useClearTokens();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["fetchAndSaveGoogleEvents"],
|
mutationKey: ["fetchAndSaveGoogleEvents"],
|
||||||
@ -25,9 +28,14 @@ export const useFetchAndSaveGoogleEvents = () => {
|
|||||||
timeMax.toISOString().slice(0, -5) + "Z"
|
timeMax.toISOString().slice(0, -5) + "Z"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(!response.success) {
|
||||||
|
await clearToken({email: email!, provider: "google"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Google Calendar events fetched:", response);
|
console.log("Google Calendar events fetched:", response);
|
||||||
|
|
||||||
const items = response?.map((item) => {
|
const items = response?.googleEvents?.map((item) => {
|
||||||
if (item.allDay) {
|
if (item.allDay) {
|
||||||
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
|
item.startDate = new Date(new Date(item.startDate).setHours(0, 0, 0, 0));
|
||||||
item.endDate = item.startDate;
|
item.endDate = item.startDate;
|
||||||
@ -41,5 +49,8 @@ export const useFetchAndSaveGoogleEvents = () => {
|
|||||||
throw error; // Ensure errors are propagated to the mutation
|
throw error; // Ensure errors are propagated to the mutation
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["events"])
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import {useMutation} from "react-query";
|
import {useMutation, useQueryClient} from "react-query";
|
||||||
import {useAuthContext} from "@/contexts/AuthContext";
|
import {useAuthContext} from "@/contexts/AuthContext";
|
||||||
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
import {useCreateEventsFromProvider} from "@/hooks/firebase/useCreateEvent";
|
||||||
import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
|
import {fetchMicrosoftCalendarEvents} from "@/calendar-integration/microsoft-calendar-utils";
|
||||||
|
|
||||||
export const useFetchAndSaveOutlookEvents = () => {
|
export const useFetchAndSaveOutlookEvents = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const {profileData} = useAuthContext();
|
const {profileData} = useAuthContext();
|
||||||
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
const {mutateAsync: createEventsFromProvider} = useCreateEventsFromProvider();
|
||||||
|
|
||||||
@ -32,5 +33,8 @@ export const useFetchAndSaveOutlookEvents = () => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["events"])
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
62
hooks/useUploadProfilePicture.ts
Normal file
62
hooks/useUploadProfilePicture.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {useState} from "react";
|
||||||
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import {useChangeProfilePicture} from "@/hooks/firebase/useChangeProfilePicture";
|
||||||
|
import {useUpdateUserData} from "@/hooks/firebase/useUpdateUserData";
|
||||||
|
|
||||||
|
export const useUploadProfilePicture = (customUserId?: string, existingPfp?: string) => {
|
||||||
|
const [profileImage, setProfileImage] = useState<
|
||||||
|
string | ImagePicker.ImagePickerAsset | null
|
||||||
|
>(existingPfp || null);
|
||||||
|
|
||||||
|
const [profileImageAsset, setProfileImageAsset] = useState<
|
||||||
|
ImagePicker.ImagePickerAsset | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const {mutateAsync: updateUserData} = useUpdateUserData();
|
||||||
|
const {mutateAsync: changeProfilePicture} = useChangeProfilePicture(customUserId);
|
||||||
|
|
||||||
|
const pickImage = async () => {
|
||||||
|
const permissionResult =
|
||||||
|
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||||
|
if (!permissionResult.granted) {
|
||||||
|
alert("Permission to access camera roll is required!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ImagePicker.launchImageLibraryAsync({
|
||||||
|
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||||
|
allowsEditing: true,
|
||||||
|
aspect: [1, 1],
|
||||||
|
quality: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.canceled) {
|
||||||
|
setProfileImage(result.assets[0].uri);
|
||||||
|
setProfileImageAsset(result.assets[0]);
|
||||||
|
if (!customUserId) {
|
||||||
|
await changeProfilePicture(result.assets[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearImage = async () => {
|
||||||
|
if (!customUserId) {
|
||||||
|
await updateUserData({newUserData: {pfp: null}});
|
||||||
|
}
|
||||||
|
setProfileImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pfpUri =
|
||||||
|
profileImage && typeof profileImage === "object" && "uri" in profileImage
|
||||||
|
? profileImage.uri
|
||||||
|
: profileImage;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pfpUri,
|
||||||
|
profileImage,
|
||||||
|
profileImageAsset,
|
||||||
|
handleClearImage,
|
||||||
|
pickImage,
|
||||||
|
changeProfilePicture
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -450,7 +450,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "Cally";
|
PRODUCT_NAME = Cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@ -484,7 +484,7 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.cally.app;
|
||||||
PRODUCT_NAME = "Cally";
|
PRODUCT_NAME = Cally;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "cally/cally-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>40</string>
|
<string>60</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
@ -118,6 +118,24 @@
|
|||||||
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
|||||||
154
package-lock.json
generated
154
package-lock.json
generated
@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "cally",
|
"name": "cally",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo-google-fonts/manrope": "^0.2.3",
|
"@expo-google-fonts/manrope": "^0.2.3",
|
||||||
"@expo-google-fonts/plus-jakarta-sans": "^0.2.3",
|
"@expo-google-fonts/plus-jakarta-sans": "^0.2.3",
|
||||||
@ -50,12 +51,14 @@
|
|||||||
"firebase-functions": "^5.1.0",
|
"firebase-functions": "^5.1.0",
|
||||||
"fuzzysort": "^3.0.2",
|
"fuzzysort": "^3.0.2",
|
||||||
"jotai": "^2.9.1",
|
"jotai": "^2.9.1",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.3",
|
||||||
"react-native-app-auth": "^8.0.0",
|
"react-native-app-auth": "^8.0.0",
|
||||||
"react-native-big-calendar": "^4.14.0",
|
"react-native-big-calendar": "^4.15.1",
|
||||||
"react-native-calendars": "^1.1306.0",
|
"react-native-calendars": "^1.1306.0",
|
||||||
|
"react-native-element-dropdown": "^2.12.2",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
"react-native-gifted-charts": "^1.4.41",
|
"react-native-gifted-charts": "^1.4.41",
|
||||||
"react-native-keyboard-manager": "^6.5.16-0",
|
"react-native-keyboard-manager": "^6.5.16-0",
|
||||||
@ -82,6 +85,7 @@
|
|||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"jest-expo": "~51.0.3",
|
"jest-expo": "~51.0.3",
|
||||||
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
}
|
}
|
||||||
@ -6013,6 +6017,12 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@yarnpkg/lockfile": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/@zxing/text-encoding": {
|
"node_modules/@zxing/text-encoding": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||||
@ -13185,6 +13195,30 @@
|
|||||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-stable-stringify": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.5",
|
||||||
|
"isarray": "^2.0.5",
|
||||||
|
"jsonify": "^0.0.1",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-stable-stringify/node_modules/isarray": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@ -13209,6 +13243,15 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonify": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
|
||||||
|
"license": "Public Domain",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||||
@ -13313,6 +13356,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/klaw-sync": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
@ -15060,6 +15112,74 @@
|
|||||||
"cross-spawn": "^7.0.3"
|
"cross-spawn": "^7.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/patch-package": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@yarnpkg/lockfile": "^1.1.0",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"ci-info": "^3.7.0",
|
||||||
|
"cross-spawn": "^7.0.3",
|
||||||
|
"find-yarn-workspace-root": "^2.0.0",
|
||||||
|
"fs-extra": "^9.0.0",
|
||||||
|
"json-stable-stringify": "^1.0.2",
|
||||||
|
"klaw-sync": "^6.0.0",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"open": "^7.4.2",
|
||||||
|
"rimraf": "^2.6.3",
|
||||||
|
"semver": "^7.5.3",
|
||||||
|
"slash": "^2.0.0",
|
||||||
|
"tmp": "^0.0.33",
|
||||||
|
"yaml": "^2.2.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"patch-package": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/open": {
|
||||||
|
"version": "7.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||||
|
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-docker": "^2.0.0",
|
||||||
|
"is-wsl": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/rimraf": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||||
|
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/patch-package/node_modules/slash": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@ -15295,6 +15415,14 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/postinstall-postinstall": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pretty-bytes": {
|
"node_modules/pretty-bytes": {
|
||||||
"version": "5.6.0",
|
"version": "5.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||||
@ -15908,13 +16036,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/react-native-big-calendar": {
|
"node_modules/react-native-big-calendar": {
|
||||||
"version": "4.14.0",
|
"version": "4.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-big-calendar/-/react-native-big-calendar-4.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-big-calendar/-/react-native-big-calendar-4.15.1.tgz",
|
||||||
"integrity": "sha512-EYCxqXnRAg8QWsW3Npq3JI/9+lXlo9o6Gri7WttQQSqE/cGkVrVeKXObpvN6Cc4qrIUvnc4cgLAeM/j4+bOb6g==",
|
"integrity": "sha512-hNrzkM+9Kb2T0J/1fW9AMaeN+AuhakCfNtQPaQL29l3JXgOO14ikJ3iPqQkmNVbuiWYiMrpI25hrmXffiOVIgQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"calendarize": "^1.1.1",
|
"calendarize": "^1.1.1",
|
||||||
"dayjs": "^1.11.10"
|
"dayjs": "^1.11.13"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
@ -15939,6 +16067,22 @@
|
|||||||
"moment": "^2.29.4"
|
"moment": "^2.29.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-element-dropdown": {
|
||||||
|
"version": "2.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-element-dropdown/-/react-native-element-dropdown-2.12.2.tgz",
|
||||||
|
"integrity": "sha512-Tf8hfRuniYEXo+LGoVgIMoItKWuPLX6jbqlwAFgMbBhmWGTuV+g1OVOAx/ny16kgnwp+NhgJoWpxhVvr7HSmXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-gesture-handler": {
|
"node_modules/react-native-gesture-handler": {
|
||||||
"version": "2.16.2",
|
"version": "2.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.16.2.tgz",
|
||||||
|
|||||||
@ -21,7 +21,8 @@
|
|||||||
"prebuild": "npx expo prebuild -p ios",
|
"prebuild": "npx expo prebuild -p ios",
|
||||||
"prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit",
|
"prebuild-build-submit-ios": "yarn run prebuild && yarn run build-ios && yarn run submit",
|
||||||
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
"prebuild-build-submit-ios-cicd": "yarn build-ios-cicd",
|
||||||
"prebuild-build-submit-cicd": "yarn build-cicd"
|
"prebuild-build-submit-cicd": "yarn build-cicd",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
@ -49,6 +50,7 @@
|
|||||||
"expo-auth-session": "^5.5.2",
|
"expo-auth-session": "^5.5.2",
|
||||||
"expo-barcode-scanner": "~13.0.1",
|
"expo-barcode-scanner": "~13.0.1",
|
||||||
"expo-build-properties": "~0.12.4",
|
"expo-build-properties": "~0.12.4",
|
||||||
|
"expo-cached-image": "^51.0.19",
|
||||||
"expo-calendar": "~13.0.5",
|
"expo-calendar": "~13.0.5",
|
||||||
"expo-camera": "~15.0.16",
|
"expo-camera": "~15.0.16",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
@ -70,11 +72,12 @@
|
|||||||
"firebase-functions": "^5.1.0",
|
"firebase-functions": "^5.1.0",
|
||||||
"fuzzysort": "^3.0.2",
|
"fuzzysort": "^3.0.2",
|
||||||
"jotai": "^2.9.1",
|
"jotai": "^2.9.1",
|
||||||
|
"patch-package": "^8.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-native": "0.74.3",
|
"react-native": "0.74.3",
|
||||||
"react-native-app-auth": "^8.0.0",
|
"react-native-app-auth": "^8.0.0",
|
||||||
"react-native-big-calendar": "^4.14.0",
|
"react-native-big-calendar": "^4.15.1",
|
||||||
"react-native-calendars": "^1.1306.0",
|
"react-native-calendars": "^1.1306.0",
|
||||||
"react-native-element-dropdown": "^2.12.2",
|
"react-native-element-dropdown": "^2.12.2",
|
||||||
"react-native-gesture-handler": "~2.16.1",
|
"react-native-gesture-handler": "~2.16.1",
|
||||||
@ -103,6 +106,7 @@
|
|||||||
"@types/react-test-renderer": "^18.0.7",
|
"@types/react-test-renderer": "^18.0.7",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
"jest-expo": "~51.0.3",
|
"jest-expo": "~51.0.3",
|
||||||
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user