Skip to content

Feat: Added banner image upload in profile section #579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading