Connect profile picture upload to backend (#533)

* Connect profile picture upload to backend

* Fix tests

* Revert onboarding state changes
This commit is contained in:
Charles Bochet
2023-07-07 17:50:02 -07:00
committed by GitHub
parent 6446692f25
commit a975935f49
21 changed files with 1522 additions and 1325 deletions

View File

@ -3,7 +3,6 @@ import {
ApolloClient,
ApolloClientOptions,
ApolloLink,
createHttpLink,
ServerError,
ServerParseError,
} from '@apollo/client';
@ -11,6 +10,7 @@ import { GraphQLErrors } from '@apollo/client/errors';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import { createUploadLink } from 'apollo-upload-client';
import { renewToken } from '@/auth/services/AuthService';
import { AuthTokenPair } from '~/generated/graphql';
@ -53,7 +53,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
this.tokenPair = tokenPair;
const buildApolloLink = (): ApolloLink => {
const httpLink = createHttpLink({
const httpLink = createUploadLink({
uri,
});

View File

@ -0,0 +1,17 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInput } from '@/ui/components/inputs/TextInput';
export function EmailField() {
const currentUser = useRecoilValue(currentUserState);
return (
<TextInput
value={currentUser?.email}
disabled
fullWidth
key={'email-' + currentUser?.id}
/>
);
}

View File

@ -0,0 +1,87 @@
import { useEffect, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import debounce from 'lodash.debounce';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { TextInput } from '@/ui/components/inputs/TextInput';
import { GET_CURRENT_USER } from '@/users/services';
import { useUpdateUserMutation } from '~/generated/graphql';
const StyledComboInputContainer = styled.div`
display: flex;
flex-direction: row;
> * + * {
margin-left: ${({ theme }) => theme.spacing(4)};
}
`;
export function NameFields() {
const currentUser = useRecoilValue(currentUserState);
const [firstName, setFirstName] = useState(currentUser?.firstName ?? '');
const [lastName, setLastName] = useState(currentUser?.lastName ?? '');
const [updateUser] = useUpdateUserMutation();
// TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
const debouncedUpdate = debounce(async () => {
try {
const { data, errors } = await updateUser({
variables: {
where: {
id: currentUser?.id,
},
data: {
firstName: {
set: firstName,
},
lastName: {
set: lastName,
},
},
},
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
});
if (errors || !data?.updateUser) {
throw errors;
}
} catch (error) {
console.error(error);
}
}, 500);
useEffect(() => {
if (
currentUser?.firstName !== firstName ||
currentUser?.lastName !== lastName
) {
debouncedUpdate();
}
return () => {
debouncedUpdate.cancel();
};
}, [firstName, lastName, currentUser, debouncedUpdate]);
return (
<StyledComboInputContainer>
<TextInput
label="First Name"
value={firstName}
onChange={setFirstName}
placeholder="Tim"
fullWidth
/>
<TextInput
label="Last Name"
value={lastName}
onChange={setLastName}
placeholder="Cook"
fullWidth
/>
</StyledComboInputContainer>
);
}

View File

@ -0,0 +1,25 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { ImageInput } from '@/ui/components/inputs/ImageInput';
import { GET_CURRENT_USER } from '@/users/services';
import { useUploadProfilePictureMutation } from '~/generated/graphql';
export function PictureUploader() {
const [uploadPicture] = useUploadProfilePictureMutation();
const [currentUser] = useRecoilState(currentUserState);
async function onUpload(file: File) {
await uploadPicture({
variables: {
file,
},
refetchQueries: [getOperationName(GET_CURRENT_USER) ?? ''],
});
}
const pictureUrl = currentUser?.avatarUrl
? `${process.env.REACT_APP_FILES_URL}/${currentUser?.avatarUrl}`
: null;
return <ImageInput picture={pictureUrl} onUpload={onUpload} />;
}

View File

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const UPDATE_PROFILE_PICTURE = gql`
mutation UploadProfilePicture($file: Upload!) {
uploadProfilePicture(file: $file)
}
`;

View File

@ -28,7 +28,6 @@ const Picture = styled.button<{ withPicture: boolean }>`
width: 66px;
img {
height: 100%;
object-fit: cover;
width: 100%;
}
@ -67,9 +66,13 @@ const Text = styled.span`
font-size: ${({ theme }) => theme.font.size.xs};
`;
const StyledHiddenFileInput = styled.input`
display: none;
`;
type Props = Omit<React.ComponentProps<'div'>, 'children'> & {
picture: string | null | undefined;
onUpload?: () => void;
onUpload?: (file: File) => void;
onRemove?: () => void;
disabled?: boolean;
};
@ -82,10 +85,18 @@ export function ImageInput({
...restProps
}: Props) {
const theme = useTheme();
const hiddenFileInput = React.useRef<HTMLInputElement>(null);
const onUploadButtonClick = () => {
hiddenFileInput.current?.click();
};
return (
<Container {...restProps}>
<Picture withPicture={!!picture} disabled={disabled} onClick={onUpload}>
<Picture
withPicture={!!picture}
disabled={disabled}
onClick={onUploadButtonClick}
>
{picture ? (
<img
src={picture || '/images/default-profile-picture.png'}
@ -97,9 +108,20 @@ export function ImageInput({
</Picture>
<Content>
<ButtonContainer>
<StyledHiddenFileInput
type="file"
ref={hiddenFileInput}
onChange={(event) => {
if (onUpload) {
if (event.target.files) {
onUpload(event.target.files[0]);
}
}
}}
/>
<Button
icon={<IconUpload size={theme.icon.size.sm} />}
onClick={onUpload}
onClick={onUploadButtonClick}
variant="secondary"
title="Upload"
disabled={disabled}
@ -110,7 +132,7 @@ export function ImageInput({
onClick={onRemove}
variant="secondary"
title="Remove"
disabled={!picture || disabled}
disabled
fullWidth
/>
</ButtonContainer>