init commit

This commit is contained in:
2025-07-07 00:52:41 +05:30
commit 2dd24927bc
81 changed files with 10274 additions and 0 deletions

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View 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>
);
};