Skip to content

Commit

Permalink
feat: add profile editing
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonioErdeljac committed Mar 5, 2023
1 parent 460bc52 commit 2330e8c
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 44 deletions.
27 changes: 17 additions & 10 deletions components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ const Avatar: React.FC<AvatarProps> = ({ userId, isLarge, hasBorder }) => {
}, [router, userId]);

return (
<div>
<div
className={`
${hasBorder ? 'border-4 border-black' : ''}
${isLarge ? 'h-32' : 'h-12'}
${isLarge ? 'w-32' : 'w-12'}
rounded-full
hover:opacity-90
transition
cursor-pointer
relative
`}
>
<Image
width={isLarge ? 128 : 48}
height={isLarge ? 128 : 48}
fill
style={{
objectFit: 'cover',
borderRadius: '100%'
}}
alt="Avatar"
onClick={onClick}
src={fetchedUser?.image || '/images/placeholder.png'}
className={`
${hasBorder ? 'border-4 border-black' : ''}
rounded-full
hover:opacity-90
transition
cursor-pointer
`}
/>
</div>
);
Expand Down
50 changes: 50 additions & 0 deletions components/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Image from "next/image";
import { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";

interface DropzoneProps {
onChange: (base64: string) => void;
label: string;
value?: string;
disabled?: boolean;
}

const Dropzone: React.FC<DropzoneProps> = ({ onChange, label, value, disabled }) => {
const [base64, setBase64] = useState(value);

const handleChange = useCallback((base64: string) => {
onChange(base64);
}, [onChange]);

const handleDrop = useCallback((files: any) => {
const file = files[0]
const reader = new FileReader();
reader.onload = (event: any) => {
setBase64(event.target.result);
handleChange(event.target.result);
};
reader.readAsDataURL(file);
}, [handleChange])

const { getRootProps, getInputProps } = useDropzone({ maxFiles: 1, onDrop: handleDrop, disabled });

return (
<div {...getRootProps({className: 'w-full p-4 text-white text-center border-2 border-dotted rounded-md border-neutral-700'})}>
<input {...getInputProps()} />
{base64 ? (
<div className="flex items-center justify-center">
<Image
src={base64}
height="100"
width="100"
alt="Uploaded image"
/>
</div>
) : (
<p className="text-white">{label}</p>
)}
</div>
);
}

export default Dropzone;
2 changes: 1 addition & 1 deletion components/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface FeedProps {
}

const Feed: React.FC<FeedProps> = ({ userId }) => {
const { data: posts = [] } = usePosts();
const { data: posts = [] } = usePosts(userId);

return (
<>
Expand Down
4 changes: 3 additions & 1 deletion components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ const Form: React.FC<FormProps> = ({ placeholder, isComment, postId }) => {
<div className="border-b-[1px] border-neutral-800 px-5 py-2">
{currentUser ? (
<div className="flex flex-row gap-4">
<Avatar userId={currentUser?.id} />
<div>
<Avatar userId={currentUser?.id} />
</div>
<div className="w-full">
<textarea
disabled={isLoading}
Expand Down
53 changes: 29 additions & 24 deletions components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,38 @@ interface InputProps {
type?: string;
disabled?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label?: string;
}

const Input: React.FC<InputProps> = ({ placeholder, value, type = "text", onChange, disabled }) => {
return (
<input
disabled={disabled}
onChange={onChange}
value={value}
placeholder={placeholder}
type={type}
className="
p-4
text-lg
bg-black
border-2
border-neutral-800
rounded-md
outline-none
text-white
focus:border-sky-500
focus:border-2
transition
disabled:bg-neutral-900
disabled:opacity-70
disabled:cursor-not-allowed
"
const Input: React.FC<InputProps> = ({ placeholder, value, type = "text", onChange, disabled, label }) => {
return (
<div className="w-full">
{label && <p className="text-xl text-white font-semibold mb-2">{label}</p>}
<input
disabled={disabled}
onChange={onChange}
value={value}
placeholder={placeholder}
type={type}
className="
w-full
p-4
text-lg
bg-black
border-2
border-neutral-800
rounded-md
outline-none
text-white
focus:border-sky-500
focus:border-2
transition
disabled:bg-neutral-900
disabled:opacity-70
disabled:cursor-not-allowed
"
/>
</div>
);
}

Expand Down
89 changes: 89 additions & 0 deletions components/modals/EditModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { toast } from "react-hot-toast";

import useCurrentUser from "@/hooks/useCurrentUser";
import useEditModal from "@/hooks/useEditModal";
import useUser from "@/hooks/useUser";

import Input from "../Input";
import Modal from "../Modal";
import Dropzone from "../Dropzone";

const EditModal = () => {
const { data: currentUser } = useCurrentUser();
const { mutate: mutateFetchedUser } = useUser(currentUser?.id);
const editModal = useEditModal();

const [image, setImage] = useState('');
const [coverImage, setCoverImage] = useState('');
const [name, setName] = useState('');
const [username, setUsername] = useState('');
const [bio, setBio] = useState('');

useEffect(() => {
setImage(currentUser?.image)
setCoverImage(currentUser?.coverImage)
setName(currentUser?.name)
setUsername(currentUser?.username)
setBio(currentUser?.bio)
}, [currentUser?.name, currentUser?.username, currentUser?.bio, currentUser?.image, currentUser?.coverImage]);

const [isLoading, setIsLoading] = useState(false);

const onSubmit = useCallback(async () => {
try {
setIsLoading(true);

await axios.patch('/api/edit', { name, username, bio, image, coverImage });
mutateFetchedUser();

toast.success('Updated');

editModal.onClose();
} catch (error) {
toast.error('Something went wrong');
} finally {
setIsLoading(false);
}
}, [editModal, name, username, bio, mutateFetchedUser, image, coverImage]);

const bodyContent = (
<div className="flex flex-col gap-4">
<Dropzone value={image} disabled={isLoading} onChange={(image) => setImage(image)} label="Upload profile image" />
<Dropzone value={coverImage} disabled={isLoading} onChange={(image) => setCoverImage(image)} label="Upload cover image" />
<Input
placeholder="Name"
onChange={(e) => setName(e.target.value)}
value={name}
disabled={isLoading}
/>
<Input
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
value={username}
disabled={isLoading}
/>
<Input
placeholder="Bio"
onChange={(e) => setBio(e.target.value)}
value={bio}
disabled={isLoading}
/>
</div>
)

return (
<Modal
disabled={isLoading}
isOpen={editModal.isOpen}
title="Edit your profile"
actionLabel="Save"
onClose={editModal.onClose}
onSubmit={onSubmit}
body={bodyContent}
/>
);
}

export default EditModal;
20 changes: 13 additions & 7 deletions components/users/UserBio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { format } from "date-fns";
import useCurrentUser from "@/hooks/useCurrentUser";
import useUser from "@/hooks/useUser";
import useFollow from "@/hooks/useFollow";
import useEditModal from "@/hooks/useEditModal";

import Button from "../Button";

Expand All @@ -16,6 +17,8 @@ const UserBio: React.FC<UserBioProps> = ({ userId }) => {
const { data: currentUser } = useCurrentUser();
const { data: fetchedUser } = useUser(userId);

const editModal = useEditModal();

const { isFollowing, toggleFollow } = useFollow(userId);

const createdAt = useMemo(() => {
Expand All @@ -30,13 +33,16 @@ const UserBio: React.FC<UserBioProps> = ({ userId }) => {
return (
<div className="border-b-[1px] border-neutral-800 pb-4">
<div className="flex justify-end p-2">
<Button
disabled={currentUser?.id === userId}
onClick={toggleFollow}
label={isFollowing ? 'Unfollow' : 'Follow'}
secondary={!isFollowing}
outline={isFollowing}
/>
{currentUser?.id === userId ? (
<Button secondary label="Edit" onClick={editModal.onOpen} />
) : (
<Button
onClick={toggleFollow}
label={isFollowing ? 'Unfollow' : 'Follow'}
secondary={!isFollowing}
outline={isFollowing}
/>
)}
</div>
<div className="mt-8 px-4">
<div className="flex flex-col">
Expand Down
9 changes: 9 additions & 0 deletions components/users/UserHero.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import Image from "next/image";

import useUser from "@/hooks/useUser";

import Avatar from "../Avatar"

interface UserHeroProps {
userId: string;
}

const UserHero: React.FC<UserHeroProps> = ({ userId }) => {
const { data: fetchedUser } = useUser(userId);

return (
<div>
<div className="bg-neutral-700 h-56 relative">
{fetchedUser?.coverImage && (
<Image src={fetchedUser.coverImage} fill alt="Cover Image" style={{ objectFit: 'cover' }}/>
)}
<div className="absolute -bottom-16 left-4">
<Avatar userId={userId} isLarge hasBorder />
</div>
Expand Down
16 changes: 16 additions & 0 deletions hooks/useEditModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { create } from 'zustand';

interface EditModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}

const useEditModal = create<EditModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false })
}));


export default useEditModal;
Loading

0 comments on commit 2330e8c

Please sign in to comment.