added Feedback page, added braindump backend

This commit is contained in:
ivic00
2024-11-02 22:31:19 +01:00
parent b9e33c3e1e
commit b35871aed8
23 changed files with 2064 additions and 1024 deletions

View File

@ -7,7 +7,7 @@ import {
DrawerItemList, DrawerItemList,
} from "@react-navigation/drawer"; } from "@react-navigation/drawer";
import { Button, View, Text, ButtonSize } from "react-native-ui-lib"; import { Button, View, Text, ButtonSize } from "react-native-ui-lib";
import { ImageBackground, 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 {
@ -30,6 +30,7 @@ import {
toDosPageIndex, toDosPageIndex,
userSettingsView, userSettingsView,
} from "@/components/pages/calendar/atoms"; } 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();
@ -52,8 +53,8 @@ export default function TabLayout() {
}} }}
drawerContent={(props) => { drawerContent={(props) => {
return ( return (
<DrawerContentScrollView {...props} style={{ height: "100%" }}> <DrawerContentScrollView {...props} style={{}}>
<View centerV margin-30 row> <View centerV marginH-30 marginT-20 marginB-20 row>
<ImageBackground <ImageBackground
source={require("../../assets/images/splash.png")} source={require("../../assets/images/splash.png")}
style={{ style={{
@ -98,6 +99,19 @@ export default function TabLayout() {
}} }}
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
@ -177,9 +191,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",
@ -243,6 +257,10 @@ export default function TabLayout() {
title: "To-Dos", title: "To-Dos",
}} }}
/> />
<Drawer.Screen
name="feedback"
options={{ drawerLabel: "Feedback", title: "Feedback" }}
/>
</Drawer> </Drawer>
); );
} }

View File

@ -0,0 +1,5 @@
import {Stack} from "expo-router";
export default function StackLayout () {
return <Stack screenOptions={{headerShown: false}}/>
}

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

View 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

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

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

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

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

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

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

View File

@ -17,7 +17,7 @@ import KeyboardManager from "react-native-keyboard-manager";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
KeyboardManager.setEnableAutoToolbar(true); if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
const SignInPage = () => { const SignInPage = () => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
@ -26,7 +26,7 @@ const SignInPage = () => {
const { mutateAsync: signIn, error, isError, isLoading } = useSignIn(); const { mutateAsync: signIn, error, isError, isLoading } = useSignIn();
const router = useRouter() const router = useRouter();
const handleSignIn = async () => { const handleSignIn = async () => {
await signIn({ email, password }); await signIn({ email, password });
@ -46,10 +46,15 @@ const SignInPage = () => {
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid> <KeyboardAwareScrollView
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}> contentContainerStyle={{ flexGrow: 1 }}
enableOnAndroid
>
<View
style={{ flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%" }}
>
<View gap-13 width={"100%"} marginB-20> <View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}> <Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
Jump back into Cally Jump back into Cally
</Text> </Text>
<Text color={"#919191"} style={{ fontSize: 20 }}> <Text color={"#919191"} style={{ fontSize: 20 }}>
@ -57,7 +62,8 @@ const SignInPage = () => {
</Text> </Text>
</View> </View>
<KeyboardAvoidingView style={{width: "100%"}} <KeyboardAvoidingView
style={{ width: "100%" }}
contentContainerStyle={{ justifyContent: "center" }} contentContainerStyle={{ justifyContent: "center" }}
keyboardVerticalOffset={50} keyboardVerticalOffset={50}
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === "ios" ? "padding" : "height"}
@ -149,8 +155,12 @@ const SignInPage = () => {
{/*</View>*/} {/*</View>*/}
{isLoading && ( {isLoading && (
<LoaderScreen overlay message={"Signing in..."} backgroundColor={Colors.white} <LoaderScreen
color={Colors.grey40}/> overlay
message={"Signing in..."}
backgroundColor={Colors.white}
color={Colors.grey40}
/>
)} )}
</View> </View>
</KeyboardAwareScrollView> </KeyboardAwareScrollView>

View File

@ -13,13 +13,13 @@ 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 {KeyboardAvoidingView, StyleSheet} from "react-native"; import { KeyboardAvoidingView, Platform, StyleSheet } from "react-native";
import { AntDesign } from "@expo/vector-icons"; import { AntDesign } from "@expo/vector-icons";
import KeyboardManager from "react-native-keyboard-manager"; import KeyboardManager from "react-native-keyboard-manager";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
KeyboardManager.setEnableAutoToolbar(true); if (Platform.OS === "ios") KeyboardManager.setEnableAutoToolbar(true);
const SignUpPage = () => { const SignUpPage = () => {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
@ -36,19 +36,24 @@ const SignUpPage = () => {
const emailRef = useRef<TextFieldRef>(null); const emailRef = useRef<TextFieldRef>(null);
const passwordRef = useRef<TextFieldRef>(null); const passwordRef = useRef<TextFieldRef>(null);
const router = useRouter() 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") router.replace("/(unauth)/cal_sync");
}; };
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView style={{ flex: 1 }}>
<KeyboardAwareScrollView contentContainerStyle={{flexGrow: 1}} enableOnAndroid> <KeyboardAwareScrollView
<View style={{flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%"}}> contentContainerStyle={{ flexGrow: 1 }}
enableOnAndroid
>
<View
style={{ flex: 1, padding: 21, paddingBottom: 45, paddingTop: "20%" }}
>
<View gap-13 width={"100%"} marginB-20> <View gap-13 width={"100%"} marginB-20>
<Text style={{fontSize: 40, fontFamily: 'Manrope_600SemiBold'}}> <Text style={{ fontSize: 40, fontFamily: "Manrope_600SemiBold" }}>
Get started with Cally Get started with Cally
</Text> </Text>
<Text color={"#919191"} style={{ fontSize: 20 }}> <Text color={"#919191"} style={{ fontSize: 20 }}>
@ -56,7 +61,7 @@ const SignUpPage = () => {
</Text> </Text>
</View> </View>
<KeyboardAvoidingView style={{width: '100%'}}> <KeyboardAvoidingView style={{ width: "100%" }}>
<TextField <TextField
marginT-30 marginT-30
autoFocus autoFocus
@ -132,7 +137,6 @@ const SignUpPage = () => {
} }
/> />
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<View gap-5 marginT-15> <View gap-5 marginT-15>
@ -157,9 +161,7 @@ const SignUpPage = () => {
onValueChange={(value) => setAcceptTerms(value)} onValueChange={(value) => setAcceptTerms(value)}
/> />
<View row style={{ flexWrap: "wrap", marginLeft: 10 }}> <View row style={{ flexWrap: "wrap", marginLeft: 10 }}>
<Text style={styles.jakartaLight}> <Text style={styles.jakartaLight}>I accept the</Text>
I accept the
</Text>
<TouchableOpacity> <TouchableOpacity>
<Text text90 style={styles.jakartaMedium}> <Text text90 style={styles.jakartaMedium}>
{" "} {" "}
@ -192,7 +194,13 @@ const SignUpPage = () => {
style={{ marginBottom: 0, height: 50 }} style={{ marginBottom: 0, height: 50 }}
/> />
<View row centerH marginT-10 marginB-2 gap-5> <View row centerH marginT-10 marginB-2 gap-5>
<Text style={[styles.jakartaLight, {fontSize: 16, color: "#484848"}]} center> <Text
style={[
styles.jakartaLight,
{ fontSize: 16, color: "#484848" },
]}
center
>
Already have an account? Already have an account?
</Text> </Text>
@ -200,7 +208,11 @@ const SignUpPage = () => {
label="Log in" label="Log in"
labelStyle={[ labelStyle={[
styles.jakartaMedium, styles.jakartaMedium,
{fontSize: 16, textDecorationLine: "none", color: "#fd1775"}, {
fontSize: 16,
textDecorationLine: "none",
color: "#fd1775",
},
]} ]}
flexS flexS
margin-0 margin-0
@ -216,8 +228,12 @@ const SignUpPage = () => {
</KeyboardAwareScrollView> </KeyboardAwareScrollView>
{isLoading && ( {isLoading && (
<LoaderScreen overlay message={"Signing up..."} backgroundColor={Colors.white} <LoaderScreen
color={Colors.grey40}/> overlay
message={"Signing up..."}
backgroundColor={Colors.white}
color={Colors.grey40}
/>
)} )}
</SafeAreaView> </SafeAreaView>
); );

View File

@ -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: "Whats For Dinner",
description:
"Whats one meal youd love to have for dinner this week?",
},
{
id: 2,
title: "The Best Thing About Today",
description:
"What was the highlight of your day? Lets 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:
"Whats a favorite memory from your childhood? Lets take a trip down memory lane and share some of the moments that made us smile.",
},
{
id: 5,
title: "A New Family Tradition",
description:
"Whats 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}

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

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

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

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

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

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

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

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

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

946
yarn.lock

File diff suppressed because it is too large Load Diff