init commit
This commit is contained in:
718
frontend/resume-builder/src/pages/ResumeUpdate/EditResume.jsx
Normal file
718
frontend/resume-builder/src/pages/ResumeUpdate/EditResume.jsx
Normal file
@ -0,0 +1,718 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
LuArrowLeft,
|
||||
LuCircleAlert,
|
||||
LuDownload,
|
||||
LuPalette,
|
||||
LuSave,
|
||||
LuTrash2,
|
||||
} from "react-icons/lu";
|
||||
import toast from "react-hot-toast";
|
||||
import DashboardLayout from "../../components/layouts/DashboardLayout";
|
||||
import TitleInput from "../../components/Inputs/TitleInput";
|
||||
import { useReactToPrint } from "react-to-print";
|
||||
import axiosInstance from "../../utils/axiosInstance";
|
||||
import { API_PATHS } from "../../utils/apiPaths";
|
||||
import StepProgress from "../../components/StepProgress";
|
||||
import ProfileInfoForm from "./Forms/ProfileInfoForm";
|
||||
import ContactInfoForm from "./Forms/ContactInfoForm";
|
||||
import WorkExperienceForm from "./Forms/WorkExperienceForm";
|
||||
import EducationDetailsForm from "./Forms/EducationDetailsForm";
|
||||
import SkillsInfoForm from "./Forms/SkillsInfoForm";
|
||||
import ProjectsDetailFrom from "./Forms/ProjectsDetailFrom";
|
||||
import CertificationInfoFrom from "./Forms/CertificationInfoFrom";
|
||||
import AdditionalInfoFrom from "./Forms/AdditionalInfoFrom";
|
||||
import RenderResume from "../../components/ResumeTemplates/RenderResume";
|
||||
import { captureElementAsImage, dataURLtoFile, fixTailwindColors } from "../../utils/helper";
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import Modal from "../../components/Modal";
|
||||
|
||||
const EditResume = () => {
|
||||
const { resumeId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const resumeRef = useRef(null);
|
||||
const resumeDownloadRef = useRef(null);
|
||||
|
||||
const [baseWidth, setBaseWidth] = useState(800);
|
||||
|
||||
const [openThemeSelector, setOpenThemeSelector] = useState(false);
|
||||
|
||||
const [openPreviewModal, setOpenPreviewModal] = useState(false);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState("profile-info");
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [resumeData, setResumeData] = useState({
|
||||
title: "",
|
||||
thumbnailLink: "",
|
||||
profileInfo: {
|
||||
profileImg: null,
|
||||
profilePreviewUrl: "",
|
||||
fullName: "",
|
||||
designation: "",
|
||||
summary: "",
|
||||
},
|
||||
template: {
|
||||
theme: "",
|
||||
colorPalette: "",
|
||||
},
|
||||
contactInfo: {
|
||||
email: "",
|
||||
phone: "",
|
||||
location: "",
|
||||
linkedin: "",
|
||||
github: "",
|
||||
website: "",
|
||||
},
|
||||
workExperience: [
|
||||
{
|
||||
company: "",
|
||||
role: "",
|
||||
startDate: "", // e.g. "2022-01"
|
||||
endDate: "", // e.g. "2023-12"
|
||||
description: "",
|
||||
},
|
||||
],
|
||||
education: [
|
||||
{
|
||||
degree: "",
|
||||
institution: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
},
|
||||
],
|
||||
skills: [
|
||||
{
|
||||
name: "",
|
||||
progress: 0, // percentage value (0-100)
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
title: "",
|
||||
description: "",
|
||||
github: "",
|
||||
liveDemo: "",
|
||||
},
|
||||
],
|
||||
certifications: [
|
||||
{
|
||||
title: "",
|
||||
issuer: "",
|
||||
year: "",
|
||||
},
|
||||
],
|
||||
languages: [
|
||||
{
|
||||
name: "",
|
||||
progress: 0, // percentage value (0-100)
|
||||
},
|
||||
],
|
||||
interests: [""],
|
||||
});
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Validate Inputs
|
||||
const validateAndNext = (e) => {
|
||||
const errors = [];
|
||||
|
||||
switch (currentPage) {
|
||||
case "profile-info":
|
||||
const { fullName, designation, summary } = resumeData.profileInfo;
|
||||
if (!fullName.trim()) errors.push("Full Name is required");
|
||||
if (!designation.trim()) errors.push("Designation is required");
|
||||
if (!summary.trim()) errors.push("Summary is required");
|
||||
break;
|
||||
|
||||
case "contact-info":
|
||||
const { email, phone } = resumeData.contactInfo;
|
||||
if (!email.trim() || !/^\S+@\S+\.\S+$/.test(email))
|
||||
errors.push("Valid email is required.");
|
||||
if (!phone.trim())
|
||||
errors.push("Valid 10-digit phone number is required");
|
||||
break;
|
||||
|
||||
case "work-experience":
|
||||
resumeData.workExperience.forEach(
|
||||
({ company, role, startDate, endDate }, index) => {
|
||||
if (!company.trim())
|
||||
errors.push(`Company is required in experience ${index + 1}`);
|
||||
if (!role.trim())
|
||||
errors.push(`Role is required in experience ${index + 1}`);
|
||||
if (!startDate || !endDate)
|
||||
errors.push(
|
||||
`Start and End dates are required in experience ${index + 1}`
|
||||
);
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case "education-info":
|
||||
resumeData.education.forEach(
|
||||
({ degree, institution, startDate, endDate }, index) => {
|
||||
if (!degree.trim())
|
||||
errors.push(`Degree is required in education ${index + 1}`);
|
||||
if (!institution.trim())
|
||||
errors.push(`Institution is required in education ${index + 1}`);
|
||||
if (!startDate || !endDate)
|
||||
errors.push(
|
||||
`Start and End dates are required in education ${index + 1}`
|
||||
);
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case "skills":
|
||||
resumeData.skills.forEach(({ name, progress }, index) => {
|
||||
if (!name.trim())
|
||||
errors.push(`Skill name is required in skill ${index + 1}`);
|
||||
if (progress < 1 || progress > 100)
|
||||
errors.push(
|
||||
`Skill progress must be between 1 and 100 in skill ${index + 1}`
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case "projects":
|
||||
resumeData.projects.forEach(({ title, description }, index) => {
|
||||
if (!title.trim())
|
||||
errors.push(`Project title is required in project ${index + 1}`);
|
||||
if (!description.trim())
|
||||
errors.push(
|
||||
`Project description is required in project ${index + 1}`
|
||||
);
|
||||
});
|
||||
break;
|
||||
|
||||
case "certifications":
|
||||
resumeData.certifications.forEach(({ title, issuer }, index) => {
|
||||
if (!title.trim())
|
||||
errors.push(
|
||||
`Certification title is required in certification ${index + 1}`
|
||||
);
|
||||
if (!issuer.trim())
|
||||
errors.push(`Issuer is required in certification ${index + 1}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case "additionalInfo":
|
||||
if (
|
||||
resumeData.languages.length === 0 ||
|
||||
!resumeData.languages[0].name?.trim()
|
||||
) {
|
||||
errors.push("At least one language is required");
|
||||
}
|
||||
|
||||
if (
|
||||
resumeData.interests.length === 0 ||
|
||||
!resumeData.interests[0]?.trim()
|
||||
) {
|
||||
errors.push("At least one interest is required");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
setErrorMsg(errors.join(", "));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to next step
|
||||
setErrorMsg("");
|
||||
goToNextStep();
|
||||
};
|
||||
|
||||
// Function to navigate to the next page
|
||||
const goToNextStep = () => {
|
||||
const pages = [
|
||||
"profile-info",
|
||||
"contact-info",
|
||||
"work-experience",
|
||||
"education-info",
|
||||
"skills",
|
||||
"projects",
|
||||
"certifications",
|
||||
"additionalInfo",
|
||||
];
|
||||
|
||||
if (currentPage === "additionalInfo") setOpenPreviewModal(true);
|
||||
|
||||
const currentIndex = pages.indexOf(currentPage);
|
||||
if (currentIndex !== -1 && currentIndex < pages.length - 1) {
|
||||
const nextIndex = currentIndex + 1;
|
||||
setCurrentPage(pages[nextIndex]);
|
||||
|
||||
// Set progress as percentage
|
||||
const percent = Math.round((nextIndex / (pages.length - 1)) * 100);
|
||||
setProgress(percent);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
// Function to navigate to the previous page
|
||||
const goBack = () => {
|
||||
const pages = [
|
||||
"profile-info",
|
||||
"contact-info",
|
||||
"work-experience",
|
||||
"education-info",
|
||||
"skills",
|
||||
"projects",
|
||||
"certifications",
|
||||
"additionalInfo",
|
||||
];
|
||||
|
||||
if (currentPage === "profile-info") navigate("/dashboard");
|
||||
|
||||
const currentIndex = pages.indexOf(currentPage);
|
||||
if (currentIndex > 0) {
|
||||
const prevIndex = currentIndex - 1;
|
||||
setCurrentPage(pages[prevIndex]);
|
||||
|
||||
// Update progress
|
||||
const percent = Math.round((prevIndex / (pages.length - 1)) * 100);
|
||||
setProgress(percent);
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
switch (currentPage) {
|
||||
case "profile-info":
|
||||
return (
|
||||
<ProfileInfoForm
|
||||
profileData={resumeData?.profileInfo}
|
||||
updateSection={(key, value) => {
|
||||
updateSection("profileInfo", key, value);
|
||||
}}
|
||||
onNext={validateAndNext}
|
||||
/>
|
||||
);
|
||||
|
||||
case "contact-info":
|
||||
return (
|
||||
<ContactInfoForm
|
||||
contactInfo={resumeData?.contactInfo}
|
||||
updateSection={(key, value) => {
|
||||
updateSection("contactInfo", key, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case "work-experience":
|
||||
return (
|
||||
<WorkExperienceForm
|
||||
workExperience={resumeData?.workExperience}
|
||||
updateArrayItem={(index, key, value) => {
|
||||
updateArrayItem("workExperience", index, key, value);
|
||||
}}
|
||||
addArrayItem={(newItem) => addArrayItem("workExperience", newItem)}
|
||||
removeArrayItem={(index) =>
|
||||
removeArrayItem("workExperience", index)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case "education-info":
|
||||
return (
|
||||
<EducationDetailsForm
|
||||
educationInfo={resumeData?.education}
|
||||
updateArrayItem={(index, key, value) => {
|
||||
updateArrayItem("education", index, key, value);
|
||||
}}
|
||||
addArrayItem={(newItem) => addArrayItem("education", newItem)}
|
||||
removeArrayItem={(index) => removeArrayItem("education", index)}
|
||||
/>
|
||||
);
|
||||
|
||||
case "skills":
|
||||
return (
|
||||
<SkillsInfoForm
|
||||
skillsInfo={resumeData?.skills}
|
||||
updateArrayItem={(index, key, value) => {
|
||||
updateArrayItem("skills", index, key, value);
|
||||
}}
|
||||
addArrayItem={(newItem) => addArrayItem("skills", newItem)}
|
||||
removeArrayItem={(index) => removeArrayItem("skills", index)}
|
||||
/>
|
||||
);
|
||||
|
||||
case "projects":
|
||||
return (
|
||||
<ProjectsDetailFrom
|
||||
projectInfo={resumeData?.projects}
|
||||
updateArrayItem={(index, key, value) => {
|
||||
updateArrayItem("projects", index, key, value);
|
||||
}}
|
||||
addArrayItem={(newItem) => addArrayItem("projects", newItem)}
|
||||
removeArrayItem={(index) => removeArrayItem("projects", index)}
|
||||
/>
|
||||
);
|
||||
|
||||
case "certifications":
|
||||
return (
|
||||
<CertificationInfoFrom
|
||||
certifications={resumeData?.certifications}
|
||||
updateArrayItem={(index, key, value) => {
|
||||
updateArrayItem("certifications", index, key, value);
|
||||
}}
|
||||
addArrayItem={(newItem) => addArrayItem("certifications", newItem)}
|
||||
removeArrayItem={(index) =>
|
||||
removeArrayItem("certifications", index)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
case "additionalInfo":
|
||||
return (
|
||||
<AdditionalInfoFrom
|
||||
languages={resumeData.languages}
|
||||
interests={resumeData.interests}
|
||||
updateArrayItem={(section, index, key, value) =>
|
||||
updateArrayItem(section, index, key, value)
|
||||
}
|
||||
addArrayItem={(section, newItem) => addArrayItem(section, newItem)}
|
||||
removeArrayItem={(section, index) =>
|
||||
removeArrayItem(section, index)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Update simple nested object (like profileInfo, contactInfo, etc.)
|
||||
const updateSection = (section, key, value) => {
|
||||
setResumeData((prev) => ({
|
||||
...prev,
|
||||
[section]: {
|
||||
...prev[section],
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
// Update array item (like workExperience[0], skills[1], etc.)
|
||||
const updateArrayItem = (section, index, key, value) => {
|
||||
setResumeData((prev) => {
|
||||
const updatedArray = [...prev[section]];
|
||||
|
||||
if (key === null) {
|
||||
updatedArray[index] = value; // for simple strings like in `interests`
|
||||
} else {
|
||||
updatedArray[index] = {
|
||||
...updatedArray[index],
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[section]: updatedArray,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Add item to array
|
||||
const addArrayItem = (section, newItem) => {
|
||||
setResumeData((prev) => ({
|
||||
...prev,
|
||||
[section]: [...prev[section], newItem],
|
||||
}));
|
||||
};
|
||||
|
||||
// Remove item from array
|
||||
const removeArrayItem = (section, index) => {
|
||||
setResumeData((prev) => {
|
||||
const updatedArray = [...prev[section]];
|
||||
updatedArray.splice(index, 1);
|
||||
return {
|
||||
...prev,
|
||||
[section]: updatedArray,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch resume info by ID
|
||||
const fetchResumeDetailsById = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get(
|
||||
API_PATHS.RESUME.GET_BY_ID(resumeId)
|
||||
);
|
||||
|
||||
if (response.data && response.data.profileInfo) {
|
||||
const resumeInfo = response.data;
|
||||
|
||||
setResumeData((prevState) => ({
|
||||
...prevState,
|
||||
title: resumeInfo?.title || "Untitled",
|
||||
template: resumeInfo?.template || prevState?.template,
|
||||
profileInfo: resumeInfo?.profileInfo || prevState?.profileInfo,
|
||||
contactInfo: resumeInfo?.contactInfo || prevState?.contactInfo,
|
||||
workExperience:
|
||||
resumeInfo?.workExperience || prevState?.workExperience,
|
||||
education: resumeInfo?.education || prevState?.education,
|
||||
skills: resumeInfo?.skills || prevState?.skills,
|
||||
projects: resumeInfo?.projects || prevState?.projects,
|
||||
certifications:
|
||||
resumeInfo?.certifications || prevState?.certifications,
|
||||
languages: resumeInfo?.languages || prevState?.languages,
|
||||
interests: resumeInfo?.interests || prevState?.interests,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching resumes:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// upload thumbnail and resume profile img
|
||||
const uploadResumeImages = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
fixTailwindColors(resumeRef.current);
|
||||
const imageDataUrl = await captureElementAsImage(resumeRef.current);
|
||||
|
||||
// Convert base64 to File
|
||||
const thumbnailFile = dataURLtoFile(
|
||||
imageDataUrl,
|
||||
`resume-${resumeId}.png`
|
||||
);
|
||||
|
||||
const profileImageFile = resumeData?.profileInfo?.profileImg || null;
|
||||
|
||||
const formData = new FormData();
|
||||
if (profileImageFile) formData.append("profileImage", profileImageFile);
|
||||
if (thumbnailFile) formData.append("thumbnail", thumbnailFile);
|
||||
|
||||
const uploadResponse = await axiosInstance.put(
|
||||
API_PATHS.RESUME.UPLOAD_IMAGES(resumeId),
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||
);
|
||||
|
||||
const { thumbnailLink, profilePreviewUrl } = uploadResponse.data;
|
||||
|
||||
console.log("RESUME_DATA___", resumeData);
|
||||
|
||||
// Call the second API to update other resume data
|
||||
await updateResumeDetails(thumbnailLink, profilePreviewUrl);
|
||||
|
||||
toast.success("Resume Updated Successfully!");
|
||||
navigate("/dashboard");
|
||||
} catch (error) {
|
||||
console.error("Error uploading images:", error);
|
||||
toast.error("Failed to upload images");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateResumeDetails = async (thumbnailLink, profilePreviewUrl) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const response = await axiosInstance.put(
|
||||
API_PATHS.RESUME.UPDATE(resumeId),
|
||||
{
|
||||
...resumeData,
|
||||
thumbnailLink: thumbnailLink || "",
|
||||
profileInfo: {
|
||||
...resumeData.profileInfo,
|
||||
profilePreviewUrl: profilePreviewUrl || "",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("Error capturing image:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete Resume
|
||||
const handleDeleteResume = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await axiosInstance.delete(API_PATHS.RESUME.DELETE(resumeId));
|
||||
toast.success('Resume Deleted Successfully')
|
||||
navigate('/dashboard')
|
||||
} catch (err) {
|
||||
console.error("Error capturing image:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// download resume
|
||||
const reactToPrintFn = useReactToPrint({ contentRef: resumeDownloadRef });
|
||||
|
||||
// Function to update baseWidth based on the resume container size
|
||||
const updateBaseWidth = () => {
|
||||
if (resumeRef.current) {
|
||||
setBaseWidth(resumeRef.current.offsetWidth);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateBaseWidth();
|
||||
window.addEventListener("resize", updateBaseWidth);
|
||||
|
||||
if (resumeId) {
|
||||
fetchResumeDetailsById();
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateBaseWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="container mx-auto">
|
||||
<div className="flex items-center justify-between gap-5 bg-white rounded-lg border border-purple-100 py-3 px-4 mb-4">
|
||||
<TitleInput
|
||||
title={resumeData.title}
|
||||
setTitle={(value) =>
|
||||
setResumeData((prevState) => ({
|
||||
...prevState,
|
||||
title: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="btn-small-light"
|
||||
onClick={() => setOpenThemeSelector(true)}
|
||||
>
|
||||
<LuPalette className="text-[16px]" />
|
||||
<span className="hidden md:block">Change Theme</span>
|
||||
</button>
|
||||
|
||||
<button className="btn-small-light" onClick={handleDeleteResume}>
|
||||
<LuTrash2 className="text-[16px]" />
|
||||
<span className="hidden md:block">Delete</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn-small-light"
|
||||
onClick={() => setOpenPreviewModal(true)}
|
||||
>
|
||||
<LuDownload className="text-[16px]" />
|
||||
<span className="hidden md:block">Preview & Download</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div className="bg-white rounded-lg border border-purple-100 overflow-hidden">
|
||||
<StepProgress progress={progress} />
|
||||
|
||||
{renderForm()}
|
||||
|
||||
<div className="mx-5">
|
||||
{errorMsg && (
|
||||
<div className="flex items-center gap-2 text-[11px] font-medium text-amber-600 bg-amber-100 px-2 py-0.5 my-1 rounded">
|
||||
<LuCircleAlert className="text-md" /> {errorMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-end justify-end gap-3 mt-3 mb-5">
|
||||
<button
|
||||
className="btn-small-light"
|
||||
onClick={goBack}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<LuArrowLeft className="text-[16px]" />
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="btn-small-light"
|
||||
onClick={uploadResumeImages}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<LuSave className="text-[16px]" />
|
||||
{isLoading ? "Updating..." : "Save & Exit"}
|
||||
</button>
|
||||
<button
|
||||
className="btn-small"
|
||||
onClick={validateAndNext}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{currentPage === "additionalInfo" && (
|
||||
<LuDownload className="text-[16px]" />
|
||||
)}
|
||||
|
||||
{currentPage === "additionalInfo"
|
||||
? "Preview & Download"
|
||||
: "Next"}
|
||||
{currentPage !== "additionalInfo" && (
|
||||
<LuArrowLeft className="text-[16px] rotate-180" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={resumeRef} className="h-[100vh]">
|
||||
{/* Resume Template */}
|
||||
<RenderResume
|
||||
templateId={resumeData?.template?.theme || ""}
|
||||
resumeData={resumeData}
|
||||
colorPalette={resumeData?.template?.colorPalette || []}
|
||||
containerWidth={baseWidth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={openThemeSelector}
|
||||
onClose={() => setOpenThemeSelector(false)}
|
||||
title="Change Theme"
|
||||
>
|
||||
<div className="w-[90vw] h-[80vh]">
|
||||
<ThemeSelector
|
||||
selectedTheme={resumeData?.template}
|
||||
setSelectedTheme={(value) => {
|
||||
setResumeData((prevState) => ({
|
||||
...prevState,
|
||||
template: value || prevState.template,
|
||||
}));
|
||||
}}
|
||||
resumeData={null}
|
||||
onClose={() => setOpenThemeSelector(false)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
isOpen={openPreviewModal}
|
||||
onClose={() => setOpenPreviewModal(false)}
|
||||
title={resumeData.title}
|
||||
showActionBtn
|
||||
actionBtnText="Download"
|
||||
actionBtnIcon={<LuDownload className="text-[16px]" />}
|
||||
onActionClick={() => reactToPrintFn()}
|
||||
>
|
||||
<div ref={resumeDownloadRef} className="w-[98vw] h-[90vh]">
|
||||
<RenderResume
|
||||
templateId={resumeData?.template?.theme || ""}
|
||||
resumeData={resumeData}
|
||||
colorPalette={resumeData?.template?.colorPalette || []}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditResume;
|
||||
@ -0,0 +1,116 @@
|
||||
import React from "react";
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import RatingInput from "../../../components/ResumeSections/RatingInput";
|
||||
|
||||
const AdditionalInfoFrom = ({
|
||||
languages,
|
||||
interests,
|
||||
updateArrayItem,
|
||||
addArrayItem,
|
||||
removeArrayItem,
|
||||
}) => {
|
||||
return <div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Additional Info</h2>
|
||||
|
||||
{/* Languages Section */}
|
||||
<div className="mt-6">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-2">Languages</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
{languages?.map((lang, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
<Input
|
||||
label="Language"
|
||||
placeholder="e.g. English"
|
||||
value={lang.name || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem("languages", index, "name", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium text-slate-600 mb-7 block">
|
||||
Proficiency
|
||||
</label>
|
||||
<RatingInput
|
||||
value={lang.progress || 0}
|
||||
onChange={(value) =>
|
||||
updateArrayItem("languages", index, "progress", value)
|
||||
}
|
||||
total={5}
|
||||
activeColor="#0ea5e9"
|
||||
inactiveColor="#e0f2fe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{languages.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem("languages", index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() => addArrayItem("languages", { name: "", progress: 0 })}
|
||||
>
|
||||
<LuPlus /> Add Language
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{console.log(interests)}
|
||||
|
||||
{/* Interests Section */}
|
||||
<div className="mt-8 mb-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700">Interests</h3>
|
||||
<div className="flex flex-col">
|
||||
{interests?.map((interest, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative rounded-lg"
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. Reading"
|
||||
value={interest || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem("interests", index, null, target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
{interests.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-6.5 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem("interests", index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() => addArrayItem("interests", "")}
|
||||
>
|
||||
<LuPlus /> Add Interest
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default AdditionalInfoFrom;
|
||||
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
const CertificationInfoFrom = ({
|
||||
certifications,
|
||||
updateArrayItem,
|
||||
addArrayItem,
|
||||
removeArrayItem,
|
||||
}) => {
|
||||
return <div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Certifications</h2>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 mb-3">
|
||||
{certifications.map((cert, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200/80 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Certificate Title"
|
||||
placeholder="Fullstack Web Developer"
|
||||
type="text"
|
||||
value={cert.title || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "title", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Issuer"
|
||||
placeholder="Coursera / Google / etc."
|
||||
type="text"
|
||||
value={cert.issuer || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "issuer", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Year"
|
||||
placeholder="2024"
|
||||
type="text"
|
||||
value={cert.year || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "year", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{certifications.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem(index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() =>
|
||||
addArrayItem({
|
||||
title: "",
|
||||
issuer: "",
|
||||
year: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuPlus /> Add Certification
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default CertificationInfoFrom;
|
||||
@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
|
||||
const ContactInfoForm = ({contactInfo, updateSection}) => {
|
||||
return (
|
||||
<div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Contact Information
|
||||
</h2>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
label="Address"
|
||||
placeholder="Short Address"
|
||||
type="text"
|
||||
value={contactInfo.location || ""}
|
||||
onChange={({ target }) => updateSection("location", target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="john@example.com"
|
||||
type="email"
|
||||
value={contactInfo.email || ""}
|
||||
onChange={({ target }) => updateSection("email", target.value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Phone Number"
|
||||
placeholder="9876543210"
|
||||
type="text"
|
||||
value={contactInfo.phone || ""}
|
||||
onChange={({ target }) => updateSection("phone", target.value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="LinkedIn"
|
||||
placeholder="https://linkedin.com/in/username"
|
||||
type="text"
|
||||
value={contactInfo.linkedin || ""}
|
||||
onChange={({ target }) => updateSection("linkedin", target.value)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="GitHub"
|
||||
placeholder="https://github.com/username"
|
||||
type="text"
|
||||
value={contactInfo.github || ""}
|
||||
onChange={({ target }) => updateSection("github", target.value)}
|
||||
/>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Input
|
||||
label="Portfolio / Website"
|
||||
placeholder="https://yourwebsite.com"
|
||||
type="text"
|
||||
value={contactInfo.website || ""}
|
||||
onChange={({ target }) => updateSection("website", target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactInfoForm
|
||||
@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
const EducationDetailsForm = ({
|
||||
educationInfo,
|
||||
updateArrayItem,
|
||||
addArrayItem,
|
||||
removeArrayItem,
|
||||
}) => {
|
||||
return <div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Education</h2>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 mb-3">
|
||||
{educationInfo.map((education, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200/80 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Degree"
|
||||
placeholder="B.Tech in Computer Science"
|
||||
type="text"
|
||||
value={education.degree || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "degree", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Institution"
|
||||
placeholder="XYZ University"
|
||||
type="text"
|
||||
value={education.institution || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "institution", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Start Date"
|
||||
type="month"
|
||||
value={education.startDate || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "startDate", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="End Date"
|
||||
type="month"
|
||||
value={education.endDate || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "endDate", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{educationInfo.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem(index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() =>
|
||||
addArrayItem({
|
||||
degree: "",
|
||||
institution: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuPlus /> Add Education
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default EducationDetailsForm;
|
||||
@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import ProfilePhotoSelector from "../../../components/Inputs/ProfilePhotoSelector";
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
|
||||
const ProfileInfoForm = ({profileData, updateSection}) => {
|
||||
return (
|
||||
<div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Personal Information
|
||||
</h2>
|
||||
|
||||
<div className="mt-4">
|
||||
<ProfilePhotoSelector
|
||||
image={profileData?.profileImg || profileData?.profilePreviewUrl}
|
||||
setImage={(value) => updateSection("profileImg", value)}
|
||||
preview={profileData?.profilePreviewUrl}
|
||||
setPreview={(value) => updateSection("profilePreviewUrl", value)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
value={profileData.fullName || ""}
|
||||
onChange={({ target }) => updateSection("fullName", target.value)}
|
||||
label="Full Name"
|
||||
placeholder="John"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<Input
|
||||
value={profileData.designation || ""}
|
||||
onChange={({ target }) =>
|
||||
updateSection("designation", target.value)
|
||||
}
|
||||
label="Designation"
|
||||
placeholder="UI Designer"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
<div className="col-span-2 mt-3">
|
||||
<label className="text-xs font-medium text-slate-600">
|
||||
Summary
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
placeholder="Short Introduction"
|
||||
className="form-input"
|
||||
rows={4}
|
||||
value={profileData.summary || ""}
|
||||
onChange={({ target }) => updateSection("summary", target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileInfoForm
|
||||
@ -0,0 +1,101 @@
|
||||
import React from "react";
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
const ProjectsDetailFrom = ({
|
||||
projectInfo,
|
||||
updateArrayItem,
|
||||
addArrayItem,
|
||||
removeArrayItem,
|
||||
}) => {
|
||||
return <div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Projects</h2>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 mb-3">
|
||||
{projectInfo.map((project, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200/80 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
label="Project Title"
|
||||
placeholder="Portfolio Website"
|
||||
type="text"
|
||||
value={project.title || ""}
|
||||
onChange={
|
||||
({ target }) =>
|
||||
updateArrayItem(index, "title", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs font-medium text-slate-600">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="Short description about the project"
|
||||
className="form-input w-full mt-1"
|
||||
rows={3}
|
||||
value={project.description || ""}
|
||||
onChange={
|
||||
({ target }) =>
|
||||
updateArrayItem(index, "description", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="GitHub Link"
|
||||
placeholder="https://github.com/username/project"
|
||||
type="url"
|
||||
value={project.github || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "github", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Live Demo URL"
|
||||
placeholder="https://yourproject.live"
|
||||
type="url"
|
||||
value={project.liveDemo || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "liveDemo", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{projectInfo.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem(index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() =>
|
||||
addArrayItem({
|
||||
title: "",
|
||||
description: "",
|
||||
github: "",
|
||||
liveDemo: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuPlus /> Add Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default ProjectsDetailFrom;
|
||||
@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import RatingInput from '../../../components/ResumeSections/RatingInput';
|
||||
|
||||
const SkillsInfoForm = ({skillsInfo, updateArrayItem, addArrayItem, removeArrayItem}) => {
|
||||
return (
|
||||
<div className="px-5 pt-3">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Skills</h2>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 mb-3">
|
||||
{skillsInfo.map((skill, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200/80 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Skill Name"
|
||||
placeholder="JavaScript"
|
||||
type="text"
|
||||
value={skill.name || ""}
|
||||
onChange={
|
||||
({ target }) =>
|
||||
updateArrayItem(index, "name", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label className="text-[13px] text-slate-800 mb-1">
|
||||
Proficiency ({skill.progress / 20 || 0}/5)
|
||||
</label>
|
||||
<div className="mt-5">
|
||||
<RatingInput
|
||||
value={skill.progress || 0}
|
||||
total={5}
|
||||
onChange={(newValue) =>
|
||||
updateArrayItem(index, "progress", newValue)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skillsInfo.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem(index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() =>
|
||||
addArrayItem({
|
||||
name: "",
|
||||
progress: 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuPlus /> Add Skill
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SkillsInfoForm
|
||||
@ -0,0 +1,103 @@
|
||||
import React from 'react'
|
||||
import Input from "../../../components/Inputs/Input";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
|
||||
const WorkExperienceForm = ({workExperience, updateArrayItem, addArrayItem, removeArrayItem}) => {
|
||||
return (
|
||||
<div className="px-5 pt-5">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Work Experience</h2>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4 mb-3">
|
||||
{workExperience.map((experience, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200/80 p-4 rounded-lg relative"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Company"
|
||||
placeholder="ABC Corp"
|
||||
type="text"
|
||||
value={experience.company || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "company", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Role"
|
||||
placeholder="Frontend Developer"
|
||||
type="text"
|
||||
value={experience.role || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "role", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Start Date"
|
||||
type="month"
|
||||
value={experience.startDate || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "startDate", target.value)
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="End Date"
|
||||
type="month"
|
||||
value={experience.endDate || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "endDate", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<label className="text-xs font-medium text-slate-600">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
placeholder="What did you do in this role?"
|
||||
className="form-input w-full mt-1"
|
||||
rows={3}
|
||||
value={experience.description || ""}
|
||||
onChange={({ target }) =>
|
||||
updateArrayItem(index, "description", target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{workExperience.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-3 right-3 text-sm text-red-600 hover:underline cursor-pointer"
|
||||
onClick={() => removeArrayItem(index)}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="self-start flex items-center gap-2 px-4 py-2 rounded bg-purple-100 text-purple-800 text-sm font-medium hover:bg-purple-200 cursor-pointer"
|
||||
onClick={() =>
|
||||
addArrayItem({
|
||||
company: "",
|
||||
role: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
description: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
<LuPlus /> Add Work Experience
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkExperienceForm
|
||||
127
frontend/resume-builder/src/pages/ResumeUpdate/ThemeSelector.jsx
Normal file
127
frontend/resume-builder/src/pages/ResumeUpdate/ThemeSelector.jsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DUMMY_RESUME_DATA,
|
||||
resumeTemplates,
|
||||
themeColorPalette,
|
||||
} from "../../utils/data";
|
||||
import { LuCircleCheckBig } from "react-icons/lu";
|
||||
import Tabs from "../../components/Tabs";
|
||||
import TemplateCard from "../../components/Cards/TemplateCard";
|
||||
import RenderResume from "../../components/ResumeTemplates/RenderResume";
|
||||
|
||||
const TAB_DATA = [{ label: "Templates" }, { label: "Color Palettes" }];
|
||||
|
||||
const ThemeSelector = ({
|
||||
selectedTheme,
|
||||
setSelectedTheme,
|
||||
resumeData,
|
||||
onClose,
|
||||
}) => {
|
||||
const resumeRef = useRef(null);
|
||||
const [baseWidth, setBaseWidth] = useState(800);
|
||||
|
||||
const [tabValue, setTabValue] = useState("Templates");
|
||||
const [selectedColorPalette, setSelectedColorPalette] = useState({
|
||||
colors: selectedTheme?.colorPalette,
|
||||
index: -1,
|
||||
});
|
||||
const [selectedTemplate, setSelectedTemplate] = useState({
|
||||
theme: selectedTheme?.theme || "",
|
||||
index: -1,
|
||||
});
|
||||
|
||||
// Handle Theme Change
|
||||
const handleThemeSelection = () => {
|
||||
setSelectedTheme({
|
||||
colorPalette: selectedColorPalette?.colors,
|
||||
theme: selectedTemplate?.theme,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
const updateBaseWidth = () => {
|
||||
if (resumeRef.current) {
|
||||
setBaseWidth(resumeRef.current.offsetWidth);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateBaseWidth();
|
||||
window.addEventListener("resize", updateBaseWidth);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", updateBaseWidth);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div className="container mx-auto px-2 md:px-0">
|
||||
<div className="flex items-center justify-between mb-5 mt-2">
|
||||
<Tabs tabs={TAB_DATA} activeTab={tabValue} setActiveTab={setTabValue} />
|
||||
|
||||
<button
|
||||
className="btn-small-light"
|
||||
onClick={() => handleThemeSelection()}
|
||||
>
|
||||
<LuCircleCheckBig className="text-[16px]" />
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 gap-5">
|
||||
<div className="col-span-12 md:col-span-5 bg-white">
|
||||
<div className="grid grid-cols-2 gap-5 max-h-[80vh] overflow-scroll custom-scrollbar md:pr-5">
|
||||
{tabValue === "Templates" &&
|
||||
resumeTemplates.map((template, index) => (
|
||||
<TemplateCard
|
||||
key={`templates_${index}`}
|
||||
thumbnailImg={template.thumbnailImg}
|
||||
isSelected={selectedTemplate?.index === index}
|
||||
onSelect={() =>
|
||||
setSelectedTemplate({ theme: template.id, index })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{tabValue === "Color Palettes" &&
|
||||
themeColorPalette.themeOne.map((colors, index) => (
|
||||
<ColorPalette
|
||||
key={`palette_${index}`}
|
||||
colors={colors}
|
||||
isSelected={selectedColorPalette?.index === index}
|
||||
onSelect={() => setSelectedColorPalette({ colors, index })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-7 bg-white -mt-3" ref={resumeRef}>
|
||||
<RenderResume
|
||||
templateId={selectedTemplate?.theme || ""}
|
||||
resumeData={resumeData || DUMMY_RESUME_DATA}
|
||||
containerWidth={baseWidth}
|
||||
colorPalette={selectedColorPalette?.colors || []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
|
||||
const ColorPalette = ({ colors, isSelected, onSelect }) => {
|
||||
return (
|
||||
<div
|
||||
className={`h-28 bg-purple-50 flex rounded-lg overflow-hidden border-2 ${
|
||||
isSelected ? "border-purple-500" : "border-none"
|
||||
}`}
|
||||
>
|
||||
{colors.map((color, index) => (
|
||||
<div
|
||||
key={`color_${index}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: colors[index] }}
|
||||
onClick={onSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user