Skip to content

Commit

Permalink
wip: change profile picture support
Browse files Browse the repository at this point in the history
  • Loading branch information
MuhammedKpln committed Mar 28, 2024
1 parent 7ab2f4f commit cbd50e0
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 64 deletions.
2 changes: 1 addition & 1 deletion app/(app)/(tabs)/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useNavigation } from 'expo-router';
import { Suspense, useLayoutEffect } from 'react';
import { Spinner } from 'tamagui';

export default function TabTwoScreen() {
export default function ProfileScreen() {
const navigation = useNavigation();
const user = useAuthStore((state) => state.user);

Expand Down
4 changes: 3 additions & 1 deletion app/(app)/profile-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Screen from '@lib/components/screen';
import { QueryKeys } from '@lib/models/query_keys.model';
import { IUser } from '@lib/models/user.model';
import ProfilePictureSection from '@lib/modules/profile-settings/ProfilePictureSection';
import { profileService } from '@lib/services/profile.service';
import { useAuthStore } from '@lib/store/auth.store';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Suspense } from 'react';
Expand All @@ -10,7 +11,8 @@ import { Spinner } from 'tamagui';
export default function ProfileSettingsPage() {
const user = useAuthStore((state) => state.user);
const profileQuery = useSuspenseQuery<IUser>({
queryKey: [QueryKeys.ProfileWithRelations, user?.id]
queryKey: [QueryKeys.ProfileWithRelations, user?.id],
queryFn: () => profileService.fetchProfile(user!.id)
});

return (
Expand Down
5 changes: 2 additions & 3 deletions lib/components/profile_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
Image,
ListItem,
Separator,
Spinner,
Text,
View,
YGroup,
Expand Down Expand Up @@ -58,10 +57,10 @@ export default function ProfileView({
>
{profileData.profileImageUrl ? (
<>
<Avatar.Image src={require('@assets/images/no_avatar.svg')} />
<Avatar.Image source={{ uri: profileData.profileImageUrl }} />

<Avatar.Fallback>
<Spinner />
<Image source={require('@assets/images/no_avatar.svg')} />
</Avatar.Fallback>
</>
) : (
Expand Down
6 changes: 6 additions & 0 deletions lib/components/switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const AppSwitchFrame = styled(SwitchFrame, {
backgroundColor: '$backgroundStrong',
borderWidth: 2,
borderColor: '$backgroundStrong'
},
true: {
borderRadius: 1000,
backgroundColor: '$backgroundStrong',
borderWidth: 2,
borderColor: '$backgroundStrong'
}
},

Expand Down
43 changes: 25 additions & 18 deletions lib/components/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Toast as IToast, useToastState } from "@tamagui/toast";
import { useMemo } from "react";
import { YStack } from "tamagui";
import { Toast as IToast, useToastState } from '@tamagui/toast';
import { useMemo } from 'react';
import { Spinner, XStack, YStack } from 'tamagui';

const Toast = () => {
const currentToast = useToastState();
const toastBgColor = useMemo(() => {
switch (currentToast?.toastType) {
case "error":
return "red";
case "success":
return "green";
case "warning":
return "orange";
case 'error':
return 'red';
case 'success':
return 'green';
case 'warning':
return 'orange';

default:
return "blue";
return 'lightblue';
}
}, [currentToast]);

Expand All @@ -32,14 +32,21 @@ const Toast = () => {
viewportName={currentToast.viewportName}
bg={toastBgColor}
>
<YStack>
<IToast.Title color="white">{currentToast.title}</IToast.Title>
{!!currentToast.message && (
<IToast.Description color="white">
{currentToast.message}
</IToast.Description>
)}
</YStack>
{currentToast.isLoading ? (
<XStack gap="$5">
<Spinner />
<IToast.Title color="white">{currentToast.title}</IToast.Title>
</XStack>
) : (
<YStack>
<IToast.Title color="white">{currentToast.title}</IToast.Title>
{!!currentToast.message && (
<IToast.Description color="white">
{currentToast.message}
</IToast.Description>
)}
</YStack>
)}
</IToast>
);
};
Expand Down
159 changes: 123 additions & 36 deletions lib/modules/profile-settings/ProfilePictureSection.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,63 @@
import NO_AVATAR from '@assets/images/no_avatar.svg';
import Button from '@lib/components/button';
import { QueryKeys } from '@lib/models/query_keys.model';
import { IUserWithPhoneAndSocial } from '@lib/models/user.model';
import { profileService } from '@lib/services/profile.service';
import { storageService } from '@lib/services/storage.service';
import { config } from '@tamagui/config/v2-reanimated';
import { Edit3 } from '@tamagui/lucide-icons';
import { useMutation } from '@tanstack/react-query';
import { useToastController } from '@tamagui/toast';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import * as ImagePicker from 'expo-image-picker';
import React, { useCallback, useState } from 'react';
import { Avatar, Card, Text, View, XStack, ZStack } from 'tamagui';
import React, { useCallback, useMemo, useState } from 'react';
import { ImageSourcePropType } from 'react-native';
import { Avatar, Card, Spinner, Text, View, XStack, YStack } from 'tamagui';
import { ISectionProps } from '.';

interface IUpdateMutation {
profileImageUrl: string;
profileImageUrl: string | null;
}

export default function ProfilePictureSection({ user }: ISectionProps) {
const [isLoading, setIsLoading] = useState(false);
const queryClient = useQueryClient();
const toast = useToastController();
const [selectedImage, setSelectedImage] =
useState<ImagePicker.ImagePickerAsset>();

const mutation = useMutation<void, void, IUpdateMutation>({
mutationFn: ({ profileImageUrl }) =>
profileService.updateProfile(user.id, { profileImageUrl })
profileService.updateProfile(user.id, { profileImageUrl }),
onSuccess: (_data, variables) => {
queryClient.setQueryData<IUserWithPhoneAndSocial>(
[QueryKeys.ProfileWithRelations, user.id],
(u) => {
if (!u) return;
return {
...u,
profileImageUrl: variables.profileImageUrl!
};
}
);
}
});

const onSave = useCallback(async () => {}, []);
const onSave = useCallback(async () => {
if (!selectedImage) return;
setIsLoading(true);

const ref = await storageService.uploadAvatar(user!.id, selectedImage);

if (ref) {
const photoURL = storageService.getAvatarURL(ref.path);

await mutation.mutateAsync({ profileImageUrl: photoURL });

toast.show('Sparat', { toastType: 'success' });
}

setIsLoading(false);
}, [selectedImage]);

const onSelect = useCallback(async () => {
let result = await ImagePicker.launchImageLibraryAsync({
Expand All @@ -38,59 +73,111 @@ export default function ProfilePictureSection({ user }: ISectionProps) {
if (result.assets && result.assets.length === 1) {
setSelectedImage(result.assets[0]);

console.log(result.assets);
await onSave();
}
}
}, []);

const onPressRemove = useCallback(async () => {
await mutation.mutateAsync({
profileImageUrl: null
});
}, []);

const profilePicture = useMemo<ImageSourcePropType>(() => {
if (selectedImage) {
return {
uri: selectedImage.uri
};
}

if (user.profileImageUrl) {
return {
uri: user.profileImageUrl
};
}

return {
uri: ''
};
}, [selectedImage, user]);

return (
<Card>
<Card.Header padded>
<Text>Profilbild</Text>
</Card.Header>

<View
animation="bouncy"
animateOnly={['transform']}
pressStyle={{
scale: 0.9
}}
justifyContent="center"
alignItems="center"
>
<ZStack>
<Avatar
circular
size="$8"
>
<Avatar.Image
source={{
uri: (selectedImage?.uri ?? user.profileImageUrl) || undefined
}}
/>
<Avatar
onPress={onSelect}
cursor="pointer"
circular
size="$8"
>
<Avatar.Image source={profilePicture} />

<Avatar.Fallback>
<NO_AVATAR
width={config.tokens.size[8].val}
height={config.tokens.size[8].val}
/>
</Avatar.Fallback>
</Avatar>
<Edit3 />
</ZStack>
<Avatar.Fallback>
<NO_AVATAR
width={config.tokens.size[8].val}
height={config.tokens.size[8].val}
/>
</Avatar.Fallback>
</Avatar>
<View
theme="subtle"
bg="$background"
p="$2"
borderRadius="$12"
position="relative"
top="$-12"
right="$-6"
zIndex={1}
>
<Edit3
size="$1"
color="black"
/>
</View>
</View>

<Card.Footer
padded
justifyContent="center"
>
<XStack>
{selectedImage && (
<Button onPress={onSelect}>Välj nytt profilbild</Button>
<YStack gap="$5">
{isLoading && (
<XStack
animation="medium"
enterStyle={{
scale: 1
}}
justifyContent="center"
gap="$3"
>
<Spinner />

<Text>Sparar...</Text>
</XStack>
)}

<Button
theme={selectedImage ? 'active' : 'subtle'}
onPress={selectedImage ? onSave : onSelect}
>
{selectedImage ? 'Spara' : 'Välj en profilbild'}
</Button>
</XStack>
{user.profileImageUrl && (
<Button
theme="active"
onPress={onPressRemove}
>
Radera
</Button>
)}
</YStack>
</Card.Footer>
</Card>
);
Expand Down
36 changes: 36 additions & 0 deletions lib/services/storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { decode } from 'base64-arraybuffer';
import { ImagePickerAsset } from 'expo-image-picker';
import mime from 'mime';
import { BaseService } from './base.service';

class StorageService extends BaseService {
async uploadAvatar(userUid: string, photo: ImagePickerAsset) {
const mimeType = photo.mimeType;

if (!mimeType) return;

const ext = mime.getExtension(photo.mimeType!);
const path = `${userUid}/avatar.${ext}`;

const { data, error } = await this.client.storage
.from('avatars')
.upload(path, decode(photo.base64!), {
upsert: true,
contentType: mimeType
});

if (error) {
throw error;
}

return data;
}

getAvatarURL(userUid: string): string {
const { data } = this.client.storage.from('avatars').getPublicUrl(userUid);

return data.publicUrl;
}
}

export const storageService = new StorageService();
1 change: 1 addition & 0 deletions lib/tamagui/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ declare module 'tamagui' {
declare module '@tamagui/toast' {
interface CustomData {
toastType?: 'error' | 'success' | 'warning';
isLoading?: boolean;
}
}
1 change: 0 additions & 1 deletion lib/tamagui/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export const colorPalette_Light = [

export const colorPalette_Dark = [
darkTransparent,
'hsl(165, 25%, 15%)',
'hsl(165, 64%, 81%)',
'hsl(165, 63%, 76%)',
'hsl(165, 62%, 71%)',
Expand Down
Loading

0 comments on commit cbd50e0

Please sign in to comment.