Profile picture upload added, QR code scan fix

This commit is contained in:
Milan Paunovic
2024-11-01 04:34:59 +01:00
parent 0fbf89644f
commit 909a4344dc
7 changed files with 784 additions and 695 deletions

View File

@ -1,33 +1,36 @@
import {SafeAreaView} from "react-native-safe-area-context";
import {Button, Colors, Dialog, LoaderScreen, Text, View} from "react-native-ui-lib";
import React, {useState} from "react";
import React, {useCallback, useState} from "react";
import {useRouter} from "expo-router";
import QRIcon from "@/assets/svgs/QRIcon";
import Toast from "react-native-toast-message";
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});
Toast.show({
type: "success",
text1: "Login successful with QR code!",
});
debouncedRouterReplace()
} catch (err) {
Toast.show({
type: "error",
text1: "Error logging in with QR code",
text2: `${err}`,
});
console.log(err)
}
};

View File

@ -225,7 +225,7 @@ export const EventCalendar: React.FC<EventCalendarProps> = React.memo(
mode={mode}
enableEnrichedEvents={true}
sortedMonthView
enrichedEventsByDate={enrichedEvents}
// enrichedEventsByDate={enrichedEvents}
events={filteredEvents}
// eventCellStyle={memoizedEventCellStyle}
onPressEvent={handlePressEvent}

View File

@ -1,27 +1,21 @@
import {
FloatingButton,
Text,
TouchableOpacity,
View,
} from "react-native-ui-lib";
import React, { useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import { ScrollView, StyleSheet } from "react-native";
import {FloatingButton, Text, TouchableOpacity, View,} from "react-native-ui-lib";
import React, {useState} from "react";
import {Ionicons} from "@expo/vector-icons";
import {ScrollView, StyleSheet} from "react-native";
import MyProfile from "./user_settings_views/MyProfile";
import MyGroup from "./user_settings_views/MyGroup";
import { useAtom } from "jotai";
import { settingsPageIndex, userSettingsView } from "../calendar/atoms";
import { AuthContextProvider } from "@/contexts/AuthContext";
import {useAtom, useSetAtom} from "jotai";
import {settingsPageIndex, userSettingsView} from "../calendar/atoms";
import PlusIcon from "@/assets/svgs/PlusIcon";
const UserSettings = () => {
const [pageIndex, setPageIndex] = useAtom(settingsPageIndex);
const setPageIndex = useSetAtom(settingsPageIndex);
const [userView, setUserView] = useAtom(userSettingsView);
const [onNewUserClick, setOnNewUserClick] = useState<(boolean)>(false);
return (
<AuthContextProvider>
<View flexG>
<ScrollView style={{ paddingBottom: 20, minHeight: "100%" }}>
<ScrollView style={{paddingBottom: 20, minHeight: "100%"}}>
<TouchableOpacity
onPress={() => {
setPageIndex(0);
@ -33,17 +27,17 @@ const UserSettings = () => {
name="chevron-back"
size={14}
color="#979797"
style={{ paddingBottom: 3 }}
style={{paddingBottom: 3}}
/>
<Text
style={{ fontFamily: "Poppins_400Regular", fontSize: 14.71 }}
style={{fontFamily: "Poppins_400Regular", fontSize: 14.71}}
color="#979797"
>
Return to main settings
</Text>
</View>
</TouchableOpacity>
<View marginH-26 flexG style={{ minHeight: "90%" }}>
<View marginH-26 flexG style={{minHeight: "90%"}}>
<Text text60R marginB-25>
User Management
</Text>
@ -79,7 +73,7 @@ const UserSettings = () => {
</View>
</TouchableOpacity>
</View>
{userView && <MyProfile />}
{userView && <MyProfile/>}
{!userView && <MyGroup onNewUserClick={onNewUserClick} setOnNewUserClick={setOnNewUserClick}/>}
</View>
</ScrollView>
@ -90,15 +84,14 @@ const UserSettings = () => {
visible
button={{
label: " Add a user device",
iconSource: () => <PlusIcon height={13} width={14} />,
iconSource: () => <PlusIcon height={13} width={14}/>,
onPress: () => setOnNewUserClick(true),
style: styles.bottomButton,
labelStyle: { fontFamily: "Manrope_600SemiBold", fontSize: 15 },
labelStyle: {fontFamily: "Manrope_600SemiBold", fontSize: 15},
}}
/>
)}
</View>
</AuthContextProvider>
);
};
@ -132,7 +125,7 @@ const styles = StyleSheet.create({
width: "50%",
borderRadius: 50,
},
title: { fontFamily: "Manrope_600SemiBold", fontSize: 18 },
title: {fontFamily: "Manrope_600SemiBold", fontSize: 18},
});
export default UserSettings;

View File

@ -3,8 +3,7 @@ import {
Button,
Card,
Colors,
Dialog,
FloatingButton,
Dialog, Image,
KeyboardAwareScrollView,
PanningProvider,
Picker,
@ -14,31 +13,31 @@ import {
TouchableOpacity,
View,
} from "react-native-ui-lib";
import React, { useEffect, useRef, useState } from "react";
import { ImageBackground, Platform, StyleSheet } from "react-native";
import { PickerSingleValue } from "react-native-ui-lib/src/components/picker/types";
import { useCreateSubUser } from "@/hooks/firebase/useCreateSubUser";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import React, {useEffect, useRef, useState} from "react";
import {ImageBackground, Platform, StyleSheet} from "react-native";
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
import { uuidv4 } from "@firebase/util";
import {uuidv4} from "@firebase/util";
import QRIcon from "@/assets/svgs/QRIcon";
import EmailIcon from "@/assets/svgs/EmailIcon";
import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import Ionicons from "@expo/vector-icons/Ionicons";
import KeyboardManager, {
PreviousNextView,
} from "react-native-keyboard-manager";
import { ScrollView } from "react-native-gesture-handler";
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";
type MyGroupProps = {
onNewUserClick: boolean;
setOnNewUserClick: React.Dispatch<React.SetStateAction<boolean>>;
};
const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick }) => {
const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) => {
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue
@ -47,6 +46,8 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [newUserId, setNewUserId] = useState("")
const lNameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
@ -54,9 +55,10 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
false
);
const { mutateAsync: createSubUser, isLoading, isError } = useCreateSubUser();
const { data: familyMembers } = useGetFamilyMembers(true);
const { user } = useAuthContext();
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
const {data: familyMembers} = useGetFamilyMembers(true);
const {user} = useAuthContext();
const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
@ -100,10 +102,17 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
setOnNewUserClick(false);
if (res?.data?.userId) {
if (profileImageAsset) {
await changeProfilePicture(profileImageAsset)
setShowQRCodeDialog(res.data.userId);
} else {
setTimeout(() => {
setShowQRCodeDialog(res.data.userId);
}, 500);
}
handleClearImage()
}
}
};
@ -147,11 +156,11 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
<ImageBackground
style={styles.pfp}
borderRadius={10.56}
source={{ uri: member.pfp || undefined }}
source={{uri: member.pfp || undefined}}
/>
) : (
<View
style={[styles.pfp, { backgroundColor: "#ea156d" }]}
style={[styles.pfp, {backgroundColor: "#ea156d"}]}
/>
)}
<View row marginL-10 centerV>
@ -159,7 +168,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
{member.firstName} {member.lastName}
</Text>
</View>
<View flexG />
<View flexG/>
<View row centerV gap-10>
<Text style={styles.userType}>
{member.userType === ProfileType.PARENT
@ -193,7 +202,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
padding-10
>
<Avatar
source={{ uri: member?.pfp ?? undefined }}
source={{uri: member?.pfp ?? undefined}}
size={40}
backgroundColor={Colors.grey60}
/>
@ -206,7 +215,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
</Text>
</View>
<View flex-1 />
<View flex-1/>
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
@ -234,7 +243,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
padding-10
>
<Avatar
source={{ uri: member?.pfp ?? undefined }}
source={{uri: member?.pfp ?? undefined}}
size={40}
backgroundColor={Colors.grey60}
/>
@ -245,7 +254,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
</Text>
</View>
<View flex-1 />
<View flex-1/>
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
@ -277,7 +286,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
</Text>
<Button backgroundColor={"#FD1775"} style={styles.dialogBtn}>
<QRIcon />
<QRIcon/>
<Text style={styles.dialogBtnLbl} marginL-7>
Show a QR Code
</Text>
@ -292,7 +301,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
}, 500);
}}
>
<EmailIcon />
<EmailIcon/>
<Text style={styles.dialogBtnLbl} marginL-7>
Enter email address
</Text>
@ -317,7 +326,7 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
<KeyboardAwareScrollView>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 16 }}>
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 16}}>
New User Information
</Text>
<TouchableOpacity
@ -325,27 +334,46 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
setOnNewUserClick(false);
}}
>
<CircledXIcon />
<CircledXIcon/>
</TouchableOpacity>
</View>
<View style={styles.divider} spread />
<View style={styles.divider} spread/>
<View row centerV gap-20 marginV-20>
{pfpUri ? (
<Image
height={65.54}
width={65.54}
style={{borderRadius: 25}}
source={{uri: pfpUri}}
/>
) : (
<View
height={65.54}
width={65.54}
children={
<ProfileIcon color={"#d6d6d6"} width={37} height={37} />
<ProfileIcon color={"#d6d6d6"} width={37} height={37}/>
}
backgroundColor={Colors.grey60}
style={{ borderRadius: 25 }}
style={{borderRadius: 25}}
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>
Upload User Profile Photo
</Text>
</TouchableOpacity>
)}
</View>
<Text style={styles.jakarta12}>Member Status</Text>
@ -369,15 +397,15 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
>
<Ionicons
name={"chevron-down"}
style={{ alignSelf: "center" }}
style={{alignSelf: "center"}}
size={20}
color={"#000000"}
/>
</View>
}
>
<Picker.Item label="Child" value={ProfileType.CHILD} />
<Picker.Item label="Parent" value={ProfileType.PARENT} />
<Picker.Item label="Child" value={ProfileType.CHILD}/>
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
<Picker.Item
label="Caregiver"
value={ProfileType.CAREGIVER}
@ -460,8 +488,8 @@ const MyGroup: React.FC<MyGroupProps> = ({ onNewUserClick, setOnNewUserClick })
fontSize: 15,
marginLeft: 7,
}}
style={{ marginTop: 20, backgroundColor: "#fd1775" }}
iconSource={() => <NavToDosIcon width={22} color={"white"} />}
style={{marginTop: 20, backgroundColor: "#fd1775"}}
iconSource={() => <NavToDosIcon width={22} color={"white"}/>}
onPress={handleCreateSubUser}
/>
</Card>
@ -477,7 +505,7 @@ const styles = StyleSheet.create({
height: 47,
width: 279,
},
dialogTitle: { fontFamily: "Manrope_600SemiBold", fontSize: 22 },
dialogTitle: {fontFamily: "Manrope_600SemiBold", fontSize: 22},
dialogBackBtn: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
@ -565,7 +593,7 @@ const styles = StyleSheet.create({
fontSize: 15,
color: "white",
},
divider: { height: 0.7, backgroundColor: "#e6e6e6", width: "100%" },
divider: {height: 0.7, backgroundColor: "#e6e6e6", width: "100%"},
jakarta12: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
@ -575,7 +603,7 @@ const styles = StyleSheet.create({
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13,
},
pfp: { aspectRatio: 1, width: 37.03, borderRadius: 10.56 },
pfp: {aspectRatio: 1, width: 37.03, borderRadius: 10.56},
userType: {
fontFamily: "Manrope_500Medium",
fontSize: 12,

View File

@ -65,7 +65,6 @@ const MyProfile = () => {
if (profileData) {
setFirstName(profileData.firstName || "");
setLastName(profileData.lastName || "");
// setProfileImage(profileData.pfp || null);
setTimeZone(
profileData.timeZone || Localization.getCalendars()[0].timeZone!
);

View File

@ -1,13 +1,13 @@
import { useMutation, useQueryClient } from "react-query";
import {useMutation, useQueryClient} from "react-query";
import firestore from "@react-native-firebase/firestore";
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 { Platform } from "react-native";
import {Platform} from "react-native";
export const useChangeProfilePicture = () => {
export const useChangeProfilePicture = (customUserId?: string) => {
const queryClient = useQueryClient();
const { user, refreshProfileData } = useAuthContext();
const {user, refreshProfileData} = useAuthContext();
return useMutation({
mutationKey: ["changeProfilePicture"],
@ -38,10 +38,12 @@ export const useChangeProfilePicture = () => {
const downloadURL = await reference.getDownloadURL();
console.log("Download URL:", downloadURL);
if(!customUserId) {
await firestore()
.collection("Profiles")
.doc(user?.uid)
.update({ pfp: downloadURL });
.update({pfp: downloadURL});
}
} catch (e) {
console.error("Error uploading profile picture:", e);
@ -50,8 +52,10 @@ export const useChangeProfilePicture = () => {
},
onSuccess: () => {
// Invalidate queries to refresh profile data
if (!customUserId) {
queryClient.invalidateQueries("Profiles");
refreshProfileData();
}
},
});
};

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