Files
cally/components/pages/settings/user_settings_views/MyProfile.tsx
2025-02-12 00:05:39 +01:00

693 lines
21 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from "react";
import { StyleSheet, TouchableOpacity } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import * as ImagePicker from "expo-image-picker";
import {
Button,
Colors,
DateTimePicker,
Image,
Picker,
Text,
TextField,
View,
} from "react-native-ui-lib";
import Ionicons from "@expo/vector-icons/Ionicons";
import * as tz from "tzdata";
import * as Localization from "expo-localization";
import debounce from "debounce";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { useUpdateUserData } from "@/hooks/firebase/useUpdateUserData";
import { useChangeProfilePicture } from "@/hooks/firebase/useChangeProfilePicture";
import { colorMap } from "@/constants/colorMap";
import DeleteProfileDialogs from "../user_components/DeleteProfileDialogs";
import { AntDesign } from "@expo/vector-icons";
import { useDeleteUser } from "@/hooks/firebase/useDeleteUser";
import { useUpdateHouseholdName } from "@/hooks/firebase/useUpdateHouseholdName";
import { useGetHouseholdName } from "@/hooks/firebase/useGetHouseholdName";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import { useDeleteFamily } from "@/hooks/firebase/useDeleteFamily";
const MyProfile = () => {
const { user, profileData } = useAuthContext();
const { data: familyMembers } = useGetFamilyMembers();
const [takenColors, setTakenColors] = useState<string[]>([]);
const { mutate: deleteFamily, isLoading } = useDeleteFamily();
const { data: hhName, refetch: refetchHHName } = useGetHouseholdName(
profileData.familyId
);
const [householdName, setHouseholdName] = useState<string>("");
const [timeZone, setTimeZone] = useState<string>(
profileData?.timeZone! ?? Localization.getCalendars()[0].timeZone
);
const [lastName, setLastName] = useState<string>(profileData?.lastName || "");
const [firstName, setFirstName] = useState<string>(
profileData?.firstName || ""
);
const [profileImage, setProfileImage] = useState<
string | ImagePicker.ImagePickerAsset | null
>(profileData?.pfp || null);
const [selectedColor, setSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const [previousSelectedColor, setPreviousSelectedColor] = useState<string>(
profileData?.eventColor ?? colorMap.pink
);
const [birthday, setBirthday] = useState<Date>(() => {
if (profileData?.birthday) {
if (profileData.birthday.toDate) {
return profileData.birthday.toDate();
}
const date = new Date(profileData.birthday);
return isNaN(date.getTime()) ? new Date() : date;
}
return new Date();
});
const [showDeleteDialog, setShowDeleteDialog] = useState<boolean>(false);
const [isDeleteFamily, setIsDeleteFamily] = useState<boolean>(false);
const handleHideDeleteDialog = () => {
setShowDeleteDialog(false);
};
const handleShowDeleteDialog = (isFamily: boolean) => {
setIsDeleteFamily(isFamily);
setShowDeleteDialog(true);
};
const { mutateAsync: updateHouseholdName } = useUpdateHouseholdName();
const { mutateAsync: updateUserData } = useUpdateUserData();
const { mutateAsync: changeProfilePicture } = useChangeProfilePicture();
const { mutateAsync: deleteAsync } = useDeleteUser();
const isFirstRender = useRef(true);
const handleUpdateUserData = async () => {
await updateUserData({ newUserData: { firstName, lastName, timeZone } });
};
const handleUpdateHouseholdName = async () => {
if (profileData?.familyId) {
await updateHouseholdName({
familyId: profileData.familyId,
name: householdName,
});
}
};
const debouncedUserDataUpdate = debounce(handleUpdateUserData, 500);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
debouncedUserDataUpdate();
}, [timeZone, lastName, firstName]);
useEffect(() => {
if (familyMembers) {
const colors = familyMembers
.filter((member) => member?.eventColor && member.uid !== user?.uid)
.map((member) => member.eventColor!);
setTakenColors(colors);
}
}, [familyMembers]);
useEffect(() => {
if (profileData) {
setFirstName(profileData.firstName || "");
setLastName(profileData.lastName || "");
setTimeZone(
profileData.timeZone || Localization.getCalendars()[0].timeZone!
);
if (profileData?.birthday) {
if (profileData.birthday.toDate) {
setBirthday(profileData.birthday.toDate());
} else {
const date = new Date(profileData.birthday);
if (!isNaN(date.getTime())) {
setBirthday(date);
}
}
}
}
}, [profileData]);
useEffect(() => {
if (profileData?.familyId) {
refetchHHName();
}
}, [profileData?.familyId]);
useEffect(() => {
setHouseholdName(hhName ?? "");
}, [hhName]);
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);
await changeProfilePicture(result.assets[0]);
}
};
const handleClearImage = async () => {
await updateUserData({ newUserData: { pfp: null } });
setProfileImage(null);
};
const pfpUri =
profileImage && typeof profileImage === "object" && "uri" in profileImage
? profileImage.uri
: profileImage;
const handleChangeColor = (color: string) => {
setPreviousSelectedColor(selectedColor);
setSelectedColor(color);
debouncedUpdateUserData(color);
};
const handleDeleteFamily = () => {
if(profileData?.familyId){
deleteFamily({familyId: profileData?.familyId}, {
onSuccess: () => {
},
onError: (error) => {
console.log("from delete fam:\n" + error);
}
})
}
}
const debouncedUpdateUserData = useCallback(
debounce(async (color: string) => {
try {
await updateUserData({
newUserData: {
eventColor: color,
},
});
} catch (error) {
console.error("Failed to update color:", error);
setSelectedColor(previousSelectedColor);
}
}, 500),
[]
);
return (
<ScrollView style={{ paddingBottom: 20, flex: 1 }}>
<View style={styles.card}>
<Text style={styles.subTit}>Your Profile</Text>
<View row spread paddingH-15 centerV marginV-15>
<TouchableOpacity onPress={pickImage}>
{pfpUri ? (
<Image
key={pfpUri}
style={[
styles.pfp,
(profileData?.eventColor && {
borderWidth: 2,
borderColor: profileData.eventColor,
}) ||
undefined,
]}
source={pfpUri ? { uri: pfpUri } : null}
/>
) : (
<View
center
style={{
aspectRatio: 1,
width: 65.54,
backgroundColor: profileData?.eventColor ?? colorMap.pink,
borderRadius: 20,
}}
>
<Text style={styles.pfpTxt}>
{profileData?.firstName?.at(0)}
{profileData?.lastName?.at(0)}
</Text>
</View>
)}
</TouchableOpacity>
<TouchableOpacity onPress={pickImage}>
<Text style={styles.photoSet} color="#50be0c" onPress={pickImage}>
{profileData?.pfp ? "Change" : "Add"} Photo
</Text>
</TouchableOpacity>
{profileData?.pfp && (
<TouchableOpacity onPress={handleClearImage}>
<Text style={styles.photoSet}>Remove Photo</Text>
</TouchableOpacity>
)}
</View>
<View paddingH-15>
{profileData?.userType == ProfileType.PARENT && (
<>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Household
</Text>
<TextField
text70
placeholder="Household name"
style={styles.txtBox}
value={householdName}
onChangeText={async (value) => {
setHouseholdName(value);
}}
onEndEditing={() => handleUpdateHouseholdName()}
/>
</>
)}
<Text text80 marginT-10 marginB-7 style={styles.label}>
First name
</Text>
<TextField
text70
placeholder="First name"
style={styles.txtBox}
value={firstName}
onChangeText={async (value) => {
setFirstName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Last name
</Text>
<TextField
text70
placeholder="Last name"
style={styles.txtBox}
value={lastName}
onChangeText={async (value) => {
setLastName(value);
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Birthday
</Text>
<DateTimePicker
style={styles.txtBox}
mode="date"
value={birthday}
onChange={(date) => {
if (date) {
const validDate = new Date(date);
if (!isNaN(validDate.getTime())) {
setBirthday(validDate);
updateUserData({
newUserData: {
birthday: validDate,
},
});
}
}
}}
/>
<Text text80 marginT-10 marginB-7 style={styles.label}>
Email address
</Text>
<TextField
editable={false}
text70
placeholder="Email address"
value={user?.email?.toString()}
style={styles.txtBox}
/>
</View>
</View>
<View style={styles.card}>
<Text style={styles.cardTitle} marginB-14>
Color Preference
</Text>
<View row spread>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.pink)}
disabled={takenColors.includes(colorMap.pink)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.pink) ? 0.1 : 1 },
]}
backgroundColor={colorMap.pink}
>
{selectedColor == colorMap.pink && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.lightPink)}
disabled={takenColors.includes(colorMap.lightPink)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.lightPink) ? 0.1 : 1 },
]}
backgroundColor={colorMap.lightPink}
>
{selectedColor == colorMap.lightPink && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.orange)}
disabled={takenColors.includes(colorMap.orange)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.orange) ? 0.1 : 1 },
]}
backgroundColor={colorMap.orange}
>
{selectedColor == colorMap.orange && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.lightOrange)}
disabled={takenColors.includes(colorMap.lightOrange)}
>
<View
style={[
styles.colorBox,
{
opacity: takenColors.includes(colorMap.lightOrange) ? 0.1 : 1,
},
]}
backgroundColor={colorMap.lightOrange}
>
{selectedColor == colorMap.lightOrange && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.green)}
disabled={takenColors.includes(colorMap.green)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.green) ? 0.1 : 1 },
]}
backgroundColor={colorMap.green}
>
{selectedColor == colorMap.green && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
</View>
<View row spread marginT-10>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.lightGreen)}
disabled={takenColors.includes(colorMap.lightGreen)}
>
<View
style={[
styles.colorBox,
{
opacity: takenColors.includes(colorMap.lightGreen) ? 0.1 : 1,
},
]}
backgroundColor={colorMap.lightGreen}
>
{selectedColor == colorMap.lightGreen && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.teal)}
disabled={takenColors.includes(colorMap.teal)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.teal) ? 0.1 : 1 },
]}
backgroundColor={colorMap.teal}
>
{selectedColor == colorMap.teal && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.lightTeal)}
disabled={takenColors.includes(colorMap.lightTeal)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.lightTeal) ? 0.1 : 1 },
]}
backgroundColor={colorMap.lightTeal}
>
{selectedColor == colorMap.lightTeal && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.purple)}
disabled={takenColors.includes(colorMap.purple)}
>
<View
style={[
styles.colorBox,
{ opacity: takenColors.includes(colorMap.purple) ? 0.1 : 1 },
]}
backgroundColor={colorMap.purple}
>
{selectedColor == colorMap.purple && (
<AntDesign name="check" size={30} color="white" />
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleChangeColor(colorMap.lightPurple)}
disabled={takenColors.includes(colorMap.lightPurple)}
>
<View
style={[
styles.colorBox,
{
opacity: takenColors.includes(colorMap.lightPurple) ? 0.1 : 1,
},
]}
backgroundColor={colorMap.lightPurple}
>
{selectedColor == colorMap.lightPurple && (
<AntDesign name="check" size={30} color="black" />
)}
</View>
</TouchableOpacity>
</View>
</View>
<View style={styles.card}>
<Text style={styles.subTit}>Settings</Text>
<Text style={styles.jakarta12}>Time Zone</Text>
<View style={styles.viewPicker}>
<Picker
value={timeZone}
onChange={(item) => setTimeZone(item as string)}
showSearch
floatingPlaceholder
style={styles.inViewPicker}
trailingAccessory={
<View
style={{
justifyContent: "center",
alignItems: "center",
height: "100%",
marginTop: -38,
paddingRight: 15,
}}
>
<Ionicons
name={"chevron-down"}
style={{ alignSelf: "center" }}
size={20}
color={"#000000"}
/>
</View>
}
>
{timeZoneItems}
</Picker>
</View>
</View>
<TouchableOpacity
onPress={() => handleShowDeleteDialog(false)}
style={{ marginTop: 10, alignSelf: "center" }}
>
<Text
style={{
color: "#ff1637",
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
}}
>
Delete Profile
</Text>
</TouchableOpacity>
{profileData?.userType === ProfileType.PARENT && (
<TouchableOpacity
onPress={() => handleShowDeleteDialog(true)}
style={{ marginTop: 20, alignSelf: "center" }}
>
<Text
style={{
color: "#ff1637",
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
}}
>
Delete Family
</Text>
</TouchableOpacity>
)}
<DeleteProfileDialogs
onFirstYes={() => {
setShowDeleteDialog(false);
}}
visible={showDeleteDialog}
onDismiss={handleHideDeleteDialog}
onConfirm={() => {
if(isDeleteFamily)
//deletes family
handleDeleteFamily();
else
{
//deletes profile
deleteAsync({});
}
}}
isDeleteFamily={isDeleteFamily}
householdName={householdName}
/>
</ScrollView>
);
};
const timeZoneItems = Object.keys(tz.zones)
.sort()
.map((zone) => (
<Picker.Item
key={zone}
label={zone.replace("/", " / ").replace("_", " ")}
value={zone}
/>
));
const styles = StyleSheet.create({
cardTitle: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
colorBox: {
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
width: 51,
borderRadius: 12,
},
card: {
marginVertical: 15,
backgroundColor: "white",
width: "100%",
borderRadius: 12,
paddingHorizontal: 20,
paddingVertical: 21,
},
pfpTxt: {
fontFamily: "Manrope_500Medium",
fontSize: 30,
color: "white",
},
pfp: {
aspectRatio: 1,
width: 65.54,
backgroundColor: "gray",
borderRadius: 20,
},
txtBox: {
backgroundColor: "#fafafa",
borderRadius: 50,
borderWidth: 2,
borderColor: "#cecece",
padding: 15,
height: 45,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13,
},
subTit: {
fontFamily: "Manrope_500Medium",
fontSize: 15,
},
label: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1",
},
photoSet: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13.07,
},
jakarta12: {
paddingVertical: 10,
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
color: "#a1a1a1",
},
viewPicker: {
borderRadius: 50,
backgroundColor: Colors.grey80,
marginBottom: 16,
borderColor: Colors.grey50,
borderWidth: 1,
marginTop: 0,
height: 40,
zIndex: 10,
},
inViewPicker: {
borderRadius: 50,
paddingVertical: 12,
paddingHorizontal: 16,
marginBottom: 16,
marginTop: -20,
height: 40,
zIndex: 10,
},
});
export default MyProfile;