Skip to content

Commit

Permalink
Add profile picture change functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
williammck committed Dec 3, 2024
1 parent 6cc0066 commit 10451bb
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 45 deletions.
97 changes: 97 additions & 0 deletions src/app/dashboard/profile/ChangeProfilePictureModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client';

import React, { useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Controller, type SubmitHandler, useForm } from 'react-hook-form';
import { toast } from 'react-toastify';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Modal, ModalButton, type ModalProps } from '@/components/Modal';
import { FileInput } from '@/components/Forms';
import { Button } from '@/components/Button';
import { fetchApi } from '@/utils/fetch';
import { type User } from '@/types/users';

const profilePictureSchema = z.object({
avatar: z.union([
z.instanceof(File, { message: 'This field is required' }),
z.string(),
]),
});

type ProfilePictureFormValues = z.infer<typeof profilePictureSchema>;

interface ChangeProfilePictureModalProps extends ModalProps {
user: User;
}

export const ChangeProfilePictureModal: React.FC<ChangeProfilePictureModalProps> = ({ user, show, close }) => {
const router = useRouter();

const { control, handleSubmit, formState: { errors, isSubmitting } } = useForm<ProfilePictureFormValues>({
resolver: zodResolver(profilePictureSchema),
});

const putProfilePicture: SubmitHandler<ProfilePictureFormValues> = useCallback((values) => {
const data = new FormData();

data.append('avatar', values.avatar);

toast.promise(
fetchApi(`/users/${user.cid}/`, { method: 'PUT', body: data }),
{
pending: 'Saving changes',
success: 'Successfully saved',
error: 'Something went wrong, check console for more info',
},
)
.then(() => {
router.refresh();
close?.();
});
}, [user, router, close]);

const resetProfilePicture = () => putProfilePicture({ avatar: '' });

return (
<Modal show={show} title="Change Profile Picture" close={close}>
<form onSubmit={handleSubmit(putProfilePicture)}>
<Controller
name="avatar"
control={control}
render={({
field: {
value,
onChange,
},
}) => (
<FileInput
className="mb-5"
error={errors.avatar?.message}
currentFile={value}
onUpload={(acceptedFiles) => onChange(acceptedFiles[0])}
/>
)}
/>

<div className="flex justify-end gap-3">
<Button color="red-400" className="mr-auto" onClick={resetProfilePicture}>
Reset
</Button>
<Button color="gray-300" onClick={close}>
Close
</Button>
<Button type="submit" disabled={isSubmitting}>
Save
</Button>
</div>
</form>
</Modal>
);
};

export const ChangeProfilePictureButton: React.FC<ChangeProfilePictureModalProps> = ({ ...props }) => (
<ModalButton modal={<ChangeProfilePictureModal {...props} />}>
Change
</ModalButton>
);
34 changes: 20 additions & 14 deletions src/app/dashboard/profile/EditProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TextAreaInput } from '@/components/Forms';
import { ProfilePicture } from '@/components/ProfilePicture';
import { fetchApi } from '@/utils/fetch';
import { type User } from '@/types/users';
import { ChangeProfilePictureButton } from './ChangeProfilePictureModal';
import { type EditProfileFormValues, editProfileSchema } from './editProfileSchema';

interface EditProfileFormProps {
Expand Down Expand Up @@ -38,22 +39,27 @@ export const EditProfileForm: React.FC<EditProfileFormProps> = ({ user }) => {
}, [user]);

return (
<form onSubmit={handleSubmit(putRequest)}>
<div className="mb-5 flex items-center gap-5">
<ProfilePicture user={user} size={64} />
<p className="italic">Profile picture updates coming soon!</p>
<>
<div className="mb-5">
<p className="mb-2 font-medium">Profile Picture</p>
<div className="flex items-center gap-5">
<ProfilePicture user={user} size={64} />
<ChangeProfilePictureButton user={user} />
</div>
</div>

<TextAreaInput
{...register('biography')}
className="mb-5"
label="Biography"
error={errors.biography?.message}
/>
<form onSubmit={handleSubmit(putRequest)}>
<TextAreaInput
{...register('biography')}
className="mb-5"
label="Biography"
error={errors.biography?.message}
/>

<Button type="submit" disabled={isSubmitting}>
Save
</Button>
</form>
<Button type="submit" disabled={isSubmitting}>
Save
</Button>
</form>
</>
);
};
2 changes: 1 addition & 1 deletion src/app/staff/StaffCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface StaffCardProps {
export const StaffCard: React.FC<StaffCardProps> = ({ user, title, description, email }) => (
<Card className="flex flex-col gap-3">
<div className="flex items-center gap-5">
<ProfilePicture user={user} size={70} alt="Vacant" />
<ProfilePicture user={user} size={70} />
<div>
<p className="text-2xl font-medium">
{user ? `${user.first_name} ${user.last_name}` : 'Vacant'}
Expand Down
48 changes: 18 additions & 30 deletions src/components/ProfilePicture.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
import React from 'react';
import Image from 'next/image';
import classNames from 'classnames';
import { LuUser } from 'react-icons/lu';
import { type BasicUser } from '@/types/users';

interface ProfilePictureProps {
user?: BasicUser;
alt?: string;
size: number;
className?: string;
}

export const ProfilePicture: React.FC<ProfilePictureProps> = ({ user, size, alt = '', className }) => {
if (!user) {
export const ProfilePicture: React.FC<ProfilePictureProps> = ({ user, size, className }) => {
if (user?.profile) {
return (
<Image
className={classNames('rounded-full bg-slate-300', className)}
src="/img/profile.png"
alt={alt}
src={`${process.env.NEXT_PUBLIC_API_URL}${user.profile}`}
alt={`${user.first_name} ${user.last_name}`}
height={size}
width={size}
/>
);
}

if (!user.profile) {
return (
<div
className={classNames(
'flex items-center justify-center rounded-full bg-slate-300',
className,
)}
style={{
width: size,
height: size,
fontSize: size / 2,
}}
>
<span className="font-medium text-slate-600">{user.initials}</span>
</div>
);
}

return (
<Image
className={classNames('rounded-full bg-slate-300', className)}
src={`${process.env.NEXT_PUBLIC_API_URL}${user.profile}`}
alt={`${user.first_name} ${user.last_name}`}
height={size}
width={size}
/>
<div
className={classNames(
'flex items-center justify-center rounded-full bg-slate-300 font-medium text-slate-600',
className,
)}
style={{
width: size,
height: size,
fontSize: size / 2,
}}
>
{user ? user.initials : <LuUser />}
</div>
);
};

0 comments on commit 10451bb

Please sign in to comment.