Skip to content

Commit

Permalink
feat: added banner image upload in profile section
Browse files Browse the repository at this point in the history
  • Loading branch information
HarshJ2508 committed Nov 7, 2024
1 parent 2ed4b46 commit 01d7636
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "banner" TEXT;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ model User {
password String?
avatar String?
banner String?
isVerified Boolean @default(false)
role Role @default(USER)
jobs Job[]
Expand Down
2 changes: 2 additions & 0 deletions src/actions/user.profile.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export const getUserDetailsWithId = async (id: string) => {
contactEmail: true,
resume: true,
avatar: true,
banner: true,
aboutMe: true,
project: true,
resumeUpdateDate: true,
Expand Down Expand Up @@ -422,6 +423,7 @@ export const updateUserDetails = withSession<
contactEmail: data.contactEmail,
aboutMe: data.aboutMe,
avatar: data.avatar,
banner: data.banner,
discordLink: data.discordLink,
linkedinLink: data.linkedinLink,
twitterLink: data.twitterLink,
Expand Down
14 changes: 13 additions & 1 deletion src/components/profile/ProfileHeroSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSession } from 'next-auth/react';
import { UserType } from '@/types/user.types';
import ProfileSocials from './ProfileSocials';
import { ProfileShareDialog } from './ProfileShare';
import Image from 'next/image';

const ProfileHeroSection = ({ userdetails }: { userdetails: UserType }) => {
const [isSheetOpen, setIsSheetOpen] = useState<boolean>(false);
Expand All @@ -30,7 +31,18 @@ const ProfileHeroSection = ({ userdetails }: { userdetails: UserType }) => {
return (
<>
<div className="border rounded-2xl min-h-72 overflow-hidden">
<div className="w-full h-32 bg-gradient-to-r from-blue-500 to-indigo-500"></div>
{userdetails.banner ? (
<div className="relative w-full h-32">
<Image
alt="banner-img"
src={userdetails.banner}
className="object-cover"
layout="fill"
/>
</div>
) : (
<div className="w-full h-32 bg-gradient-to-r from-blue-500 to-indigo-500"></div>
)}
<div className="p-6 relative flex-col flex gap-y-3">
<Avatar className="h-32 w-32 absolute -top-16 bg-slate-100 dark:bg-slate-900">
{userdetails.avatar && (
Expand Down
173 changes: 121 additions & 52 deletions src/components/profile/forms/EditProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useRef, useState } from 'react';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
Expand All @@ -19,7 +18,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { uploadFileAction } from '@/actions/upload-to-cdn';
import { X } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { UserType } from '@/types/user.types';
import { updateUserDetails } from '@/actions/user.profile.actions';
Expand All @@ -33,18 +31,22 @@ const EditProfileForm = ({
}) => {
const { toast } = useToast();

const [file, setFile] = useState<File | null>(null);
const [previewImg, setPreviewImg] = useState<string | null>(
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [bannerFile, setBannerFile] = useState<File | null>(null);
const [previewAvatarImg, setPreviewAvatarImg] = useState<string | null>(
userdetails.avatar || null
);
const [previewBannerImg, setPreviewBannerImg] = useState<string | null>(
userdetails.banner || null
);

const form = useForm<ProfileSchemaType>({
resolver: zodResolver(profileSchema),
defaultValues: {
aboutMe: userdetails.aboutMe || '',
email: userdetails.email || '',
contactEmail: userdetails.contactEmail || '',
avatar: userdetails.avatar || '',
banner: userdetails.banner || '',
name: userdetails.name || '',
discordLink: userdetails.discordLink || '',
linkedinLink: userdetails.linkedinLink || '',
Expand Down Expand Up @@ -78,8 +80,11 @@ const EditProfileForm = ({

const onSubmit = async (data: ProfileSchemaType) => {
try {
if (file) {
data.avatar = (await submitImage(file)) ?? '/main.svg';
if (avatarFile) {
data.avatar = (await submitImage(avatarFile)) ?? '/main.svg';
}
if (bannerFile) {
data.banner = (await submitImage(bannerFile)) ?? '';
}
const response = await updateUserDetails(data);

Expand Down Expand Up @@ -108,27 +113,50 @@ const EditProfileForm = ({
handleClose();
};
const profileImageRef = useRef<HTMLImageElement>(null);
const bannerImageRef = useRef<HTMLImageElement>(null);

const handleClick = (inputType: string) => {
if (!inputType) return;
if (
(inputType === 'avatarFileInput' && previewAvatarImg) ||
(inputType === 'bannerFileInput' && previewBannerImg)
)
return;

const handleClick = () => {
if (previewImg) return;
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const fileInput = document.getElementById(
`${inputType}`
) as HTMLInputElement;

if (fileInput) {
fileInput.click();
}
};

const clearLogoImage = () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const clearLogoImage = (inputType: string) => {
if (!inputType) return;
const fileInput = document.getElementById(
`${inputType}`
) as HTMLInputElement;

if (fileInput) {
fileInput.value = '';
}
setPreviewImg(null);
setFile(null);
form.setValue('avatar', '');
if (inputType === 'avatarFileInput') {
setPreviewAvatarImg(null);
setAvatarFile(null);
form.setValue('avatar', '');
} else if (inputType === 'bannerFileInput') {
setPreviewBannerImg(null);
setBannerFile(null);
form.setValue('banner', '');
}
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {

const handleFileChange = async (
e: React.ChangeEvent<HTMLInputElement>,
inputType: string
) => {
if (!inputType) return;
const selectedFile = e.target.files ? e.target.files[0] : null;
if (!selectedFile) {
return;
Expand All @@ -143,14 +171,22 @@ const EditProfileForm = ({
}
const reader = new FileReader();
reader.onload = () => {
if (profileImageRef.current) {
profileImageRef.current.src = reader.result as string;
if (inputType === 'bannerFileInput') {
if (bannerImageRef.current) {
bannerImageRef.current.src = reader.result as string;
}
setPreviewBannerImg(reader.result as string);
} else {
if (profileImageRef.current) {
profileImageRef.current.src = reader.result as string;
}
setPreviewAvatarImg(reader.result as string);
}
setPreviewImg(reader.result as string);
};
reader.readAsDataURL(selectedFile);
if (selectedFile) {
setFile(selectedFile);
if (inputType === 'bannerFileInput') setBannerFile(selectedFile);
else setAvatarFile(selectedFile);
}
};

Expand All @@ -161,32 +197,84 @@ const EditProfileForm = ({
className="space-y-8 h-full flex flex-col justify-between"
>
<div className="flex flex-col gap-y-4">
<FormLabel> Banner Picture </FormLabel>
<div className="flex justify-center">
<div
className="w-full h-28 relative dark:bg-gray-700 bg-gray-300 border border-dashed border-gray-500 rounded-md flex items-center justify-center cursor-pointer mb-2"
onClick={() => handleClick('bannerFileInput')}
>
{previewBannerImg ? (
<Image
src={previewBannerImg}
ref={bannerImageRef}
className="object-cover w-full h-full rounded-md overflow-hidden"
alt="Banner Logo"
width={160}
height={160}
/>
) : (
<div className="flex flex-col justify-center items-center gap-2">
<FaFileUpload
height={80}
width={80}
className="text-white h-10 w-10"
/>
<div className="text-sm text-gray-400 dark:text-gray-300 text-center px-4">
Upload banner image
</div>
</div>
)}
{previewBannerImg && (
<button
type="button"
onClick={() => clearLogoImage('bannerFileInput')}
className="absolute top-0 right-0 w-5 h-5 bg-red-500 rounded-full items-center flex justify-center cursor-pointer translate-x-1/2 -translate-y-1/2"
>
<X size="16" />
</button>
)}
</div>

<input
id="bannerFileInput"
className="hidden"
type="file"
accept="image/*"
onChange={(event) => handleFileChange(event, 'bannerFileInput')}
/>
</div>

<FormLabel> Profile Picture </FormLabel>
<div className="flex justify-center">
<div
className="w-40 h-40 relative dark:bg-gray-700 bg-gray-300 border border-dashed border-gray-500 rounded-md flex items-center justify-center cursor-pointer mb-2"
onClick={handleClick}
onClick={() => handleClick('avatarFileInput')}
>
{previewImg ? (
{previewAvatarImg ? (
<Image
src={previewImg}
src={previewAvatarImg}
ref={profileImageRef}
className="object-contain w-full h-full rounded-md overflow-hidden"
className="object-cover w-full h-full rounded-md overflow-hidden"
alt="Company Logo"
width={160}
height={160}
/>
) : (
<FaFileUpload
height={80}
width={80}
className="text-white h-10 w-10"
/>
<div className="flex flex-col justify-center items-center gap-2">
<FaFileUpload
height={80}
width={80}
className="text-white h-10 w-10"
/>
<div className="text-sm text-gray-400 dark:text-gray-300 text-center px-4">
Upload profile image
</div>
</div>
)}
{previewImg && (
{previewAvatarImg && (
<button
type="button"
onClick={clearLogoImage}
onClick={() => clearLogoImage('avatarFileInput')}
className="absolute top-0 right-0 w-5 h-5 bg-red-500 rounded-full items-center flex justify-center cursor-pointer translate-x-1/2 -translate-y-1/2"
>
<X size="16" />
Expand All @@ -195,11 +283,11 @@ const EditProfileForm = ({
</div>

<input
id="fileInput"
id="avatarFileInput"
className="hidden"
type="file"
accept="image/*"
onChange={handleFileChange}
onChange={(event) => handleFileChange(event, 'avatarFileInput')}
/>
</div>

Expand Down Expand Up @@ -332,25 +420,6 @@ const EditProfileForm = ({
</FormItem>
)}
/>
<FormField
control={form.control}
name="aboutMe"
render={({ field }) => (
<FormItem>
<FormLabel>About Me</FormLabel>
<FormControl>
<Textarea
placeholder="Write here"
{...field}
className="rounded-[8px]"
/>
</FormControl>
<FormDescription>
Describe yourself between 50 to 255 characters.
</FormDescription>
</FormItem>
)}
/>
</div>
<div className="py-4 flex gap-4 justify-end">
<Button
Expand Down
1 change: 1 addition & 0 deletions src/lib/validators/user.profile.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const aboutMeSchema = z.object({

export const profileSchema = z.object({
avatar: z.string().optional(),
banner: z.string().optional(),
name: z.string().min(1, 'Name is required'),
email: z.string().min(1, 'Email is required').email(),
contactEmail: z.string().email().optional().or(z.literal('')),
Expand Down
1 change: 1 addition & 0 deletions src/types/user.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface UserType {
contactEmail: string | null;
resume: string | null;
avatar: string | null;
banner: string | null;
aboutMe: string | null;
experience: ExperienceType[];
education: EducationType[];
Expand Down

0 comments on commit 01d7636

Please sign in to comment.