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

BIN
frontend/.DS_Store vendored Normal file

Binary file not shown.

24
frontend/resume-builder/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

View File

@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Resume Builder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3706
frontend/resume-builder/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "resume-builder",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.4",
"axios": "^1.8.4",
"html2canvas": "^1.4.1",
"moment": "^2.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hot-toast": "^2.5.2",
"react-icons": "^5.5.0",
"react-router-dom": "^7.5.1",
"react-to-print": "^3.0.6",
"tailwindcss": "^4.1.4"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"vite": "^6.3.1"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,35 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { Toaster } from "react-hot-toast";
import LandingPage from "./pages/LandingPage";
import Dashboard from "./pages/Home/Dashboard";
import EditResume from "./pages/ResumeUpdate/EditResume";
import UserProvider from "./context/userContext";
const App = () => {
return (
<UserProvider>
<div>
<Router>
<Routes>
{/* Default Route */}
<Route path="/" element={<LandingPage />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/resume/:resumeId" element={<EditResume />} />
</Routes>
</Router>
</div>
<Toaster
toastOptions={{
className: "",
style: {
fontSize: "13px",
},
}}
/>
</UserProvider>
);
};
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -0,0 +1,39 @@
import React, { useContext } from "react";
import { UserContext } from "../../context/userContext";
import { useNavigate } from "react-router-dom";
const ProfileInfoCard = () => {
const { user, clearUser } = useContext(UserContext);
const navigate = useNavigate();
const handelLogout = () => {
localStorage.clear();
clearUser();
navigate("/");
};
return (
user && (
<div className="flex items-center">
<img
src={user.profileImageUrl}
alt=""
className="w-11 h-11 bg-gray-300 rounded-full mr-3"
/>
<div>
<div className="text-[15px] font-bold leading-3">
{user.name || ""}
</div>
<button
className="text-purple-500 text-sm font-semibold cursor-pointer hover:underline"
onClick={handelLogout}
>
Logout
</button>
</div>
</div>
)
);
};
export default ProfileInfoCard;

View File

@ -0,0 +1,46 @@
import React, { useEffect, useState } from "react";
import { getLightColorFromImage } from "../../utils/helper";
const ResumeSummaryCard = ({ imgUrl, title, lastUpdated, onSelect }) => {
const [bgColor, setBgColor] = useState("#ffffff");
useEffect(() => {
if (imgUrl) {
getLightColorFromImage(imgUrl)
.then((color) => {
setBgColor(color);
})
.catch(() => {
setBgColor("#ffffff");
});
}
}, [imgUrl]);
return <div
className="h-[300px] flex flex-col items-center justify-between bg-white rounded-lg border border-gray-200 hover:border-purple-300 overflow-hidden cursor-pointer"
style={{backgroundColor: bgColor}}
onClick={onSelect}
>
<div className="p-4">
{imgUrl ? (
<img
src={imgUrl}
alt=""
className="w-[100%] h-[200px] rounded"
/>
) : (
<div></div>
)}
</div>
<div className="w-full bg-white px-4 py-3">
<h5 className="text-sm font-medium truncate overflow-hidden whitespace-nowrap">{title}</h5>
<p className="text-xs font-medium text-gray-500 mt-0.5">
Last Updated: {lastUpdated}
</p>
</div>
</div>
};
export default ResumeSummaryCard;

View File

@ -0,0 +1,17 @@
import React from "react";
const TemplateCard = ({ thumbnailImg, isSelected, onSelect }) => {
return <div
className={`h-auto md:h-[300px] flex flex-col items-center justify-between bg-white rounded-lg border border-gray-200 hover:border-purple-300 overflow-hidden cursor-pointer
${isSelected ? "border-purple-500 border-2" : ""}`}
onClick={onSelect}
>
{thumbnailImg ? (
<img src={thumbnailImg} alt="" className="w-[100%] rounded" />
) : (
<div></div>
)}
</div>
};
export default TemplateCard;

View File

@ -0,0 +1,46 @@
import React, { useState } from "react";
import { FaRegEye, FaRegEyeSlash } from "react-icons/fa6";
const Input = ({ value, onChange, label, placeholder, type }) => {
const [showPassword, setShowPassword] = useState(false);
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
return <div>
<label className="text-[13px] text-slate-800">{label}</label>
<div className="input-box">
<input
type={
type == "password" ? (showPassword ? "text" : "password") : type
}
placeholder={placeholder}
className="w-full bg-transparent outline-none"
value={value}
onChange={(e) => onChange(e)}
/>
{type === "password" && (
<>
{showPassword ? (
<FaRegEye
size={22}
className="text-primary cursor-pointer"
onClick={() => toggleShowPassword()}
/>
) : (
<FaRegEyeSlash
size={22}
className="text-slate-400 cursor-pointer"
onClick={() => toggleShowPassword()}
/>
)}
</>
)}
</div>
</div>
};
export default Input;

View File

@ -0,0 +1,77 @@
import React, { useRef, useState } from 'react'
import { LuUser, LuUpload, LuTrash } from "react-icons/lu";
const ProfilePhotoSelector = ({image, setImage, preview, setPreview}) => {
const inputRef = useRef(null);
const [previewUrl, setPreviewUrl] = useState(null);
const handleImageChange = (event) => {
const file = event.target.files[0];
if (file) {
// Update the image state
setImage(file);
// Generate preview URL from the file
const preview = URL.createObjectURL(file);
if(setPreview){
setPreview(preview)
}
setPreviewUrl(preview);
}
};
const handleRemoveImage = () => {
setImage(null);
setPreviewUrl(null);
if(setPreview){
setPreview(null)
}
};
const onChooseFile = () => {
inputRef.current.click();
};
return (
<div className="flex justify-center mb-6">
<input
type="file"
accept="image/*"
ref={inputRef}
onChange={handleImageChange}
className="hidden"
/>
{!image ? (
<div className="w-20 h-20 flex items-center justify-center bg-purple-50 rounded-full relative cursor-pointer">
<LuUser className="text-4xl text-purple-500" />
<button
type="button"
className="w-8 h-8 flex items-center justify-center bg-linear-to-r from-purple-500/85 to-purple-700 text-white rounded-full absolute -bottom-1 -right-1 cursor-pointer"
onClick={onChooseFile}
>
<LuUpload />
</button>
</div>
) : (
<div className="relative">
<img
src={preview || previewUrl}
alt="profile photo"
className="w-20 h-20 rounded-full object-cover"
/>
<button
type="button"
className="w-8 h-8 flex items-center justify-center bg-red-500 text-white rounded-full absolute -bottom-1 -right-1 cursor-pointer"
onClick={handleRemoveImage}
>
<LuTrash />
</button>
</div>
)}
</div>
)
}
export default ProfilePhotoSelector

View File

@ -0,0 +1,39 @@
import React, { useState } from "react";
import { LuCheck, LuPencil } from "react-icons/lu";
const TitleInput = ({ title, setTitle }) => {
const [showInput, setShowInput] = useState(false);
return <div className="flex items-center gap-3">
{showInput ? (
<>
<input
type="text"
placeholder="Resume title"
className="text-sm md:text-[17px] bg-transparent outline-none text-black font-semibold border-b border-gray-300 pb-1"
value={title}
onChange={({ target }) => setTitle(target.value)}
/>
<button className="cursor-pointer">
<LuCheck
className="text-[16px] text-purple-600"
onClick={() => setShowInput((prevState) => !prevState)}
/>
</button>
</>
) : (
<>
<h2 className="text-sm md:text-[17px] font-semibold">{title}</h2>
<button className="cursor-pointer">
<LuPencil
className="text-sm text-purple-600"
onClick={() => setShowInput((prevState) => !prevState)}
/>
</button>
</>
)}
</div>
};
export default TitleInput;

View File

@ -0,0 +1,71 @@
import React from "react";
const Modal = ({
children,
isOpen,
onClose,
title,
hideHeader,
showActionBtn,
actionBtnIcon = null,
actionBtnText,
onActionClick,
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex justify-center items-center w-full h-full bg-black/40">
{/* Modal Content */}
<div
className={`relative flex flex-col bg-white shadow-lg rounded-lg overflow-hidden
`}
>
{/* Modal Header */}
{!hideHeader && (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="md:text-lg font-medium text-gray-900">{title}</h3>
{showActionBtn && (
<button
className="btn-small-light mr-12"
onClick={() => onActionClick()}
>
{actionBtnIcon}
{actionBtnText}
</button>
)}
</div>
)}
<button
type="button"
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 flex justify-center items-center absolute top-3.5 right-3.5"
onClick={onClose}
>
<svg
className="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M1 1l6 6m0 0l6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
</button>
{/* Modal Body (Scrollable) */}
<div className="flex-1 overflow-y-auto custom-scrollbar">
{children}
</div>
</div>
</div>
);
};
export default Modal;

View File

@ -0,0 +1,24 @@
import React from "react";
const Progress = ({ progress = 0, total = 5, color, bgColor }) => {
return (
<div className="flex gap-1.5">
{[...Array(total)].map((_, index) => (
<div
key={index}
className={`w-2 h-2 rounded transition-all ${
index < progress ? "bg-cyan-500 " : "bg-cyan-100"
}`}
style={{
backgroundColor:
index < progress
? color || "rgba(1,1,1,1)"
: bgColor || "rgba(1,1,1,0.1)",
}}
></div>
))}
</div>
);
};
export default Progress;

View File

@ -0,0 +1,18 @@
import React from 'react'
const ActionLink = ({icon, link, bgColor}) => {
return (
<div className="flex items-center gap-3">
<div
className="w-[25px] h-[25px] flex items-center justify-center rounded-full"
style={{ backgroundColor: bgColor }}
>
{icon}
</div>
<p className="text-[13px] font-medium underline cursor-pointer break-all">{link}</p>
</div>
)
}
export default ActionLink

View File

@ -0,0 +1,22 @@
import React from "react";
const CertificationInfo = ({ title, issuer, year, bgColor }) => {
return <div className="">
<h3 className={`text-[15px] font-semibold text-gray-900`}>{title}</h3>
<div className="flex items-center gap-2">
{year && (
<div
className="text-[11px] font-bold text-gray-800 px-3 py-0.5 inline-block mt-2 rounded-lg"
style={{ backgroundColor: bgColor }}
>
{year}
</div>
)}
<p className="text-[12px] text-gray-700 font-medium mt-1">{issuer}</p>
</div>
</div>
};
export default CertificationInfo;

View File

@ -0,0 +1,16 @@
import React from "react";
const ContactInfo = ({ icon, iconBG, value }) => {
return <div className="flex items-center gap-3">
<div
className="w-[30px] h-[30px] flex items-center justify-center rounded-full"
style={{ backgroundColor: iconBG }}
>
{icon}
</div>
<p className="flex-1 text-[12px] font-medium break-all">{value}</p>
</div>
};
export default ContactInfo;

View File

@ -0,0 +1,13 @@
import React from "react";
const EducationInfo = ({ degree, institution, duration }) => {
return <div className="mb-5">
<h3 className={`text-[15px] font-semibold text-gray-900`}>{degree}</h3>
<p className="text-sm text-gray-700 font-medium">{institution}</p>
<p className="text-xs text-gray-500 font-medium italic mt-0.5">
{duration}
</p>
</div>
};
export default EducationInfo;

View File

@ -0,0 +1,33 @@
import React from "react";
import Progress from "../Progress";
const LanguageInfo = ({ language, progress, accentColor, bgColor }) => {
return (
<div className="flex items-center justify-between">
<p className={`text-[12px] font-semibold text-gray-900`}>{language}</p>
{progress > 0 && (
<Progress
progress={(progress / 100) * 5}
color={accentColor}
bgColor={bgColor}
/>
)}
</div>
);
};
const LanguageSection = ({ languages, accentColor, bgColor }) => {
return <div className="flex flex-col gap-2">
{languages?.map((language, index) => (
<LanguageInfo
key={`slanguage_${index}`}
language={language.name}
progress={language.progress}
accentColor={accentColor}
bgColor={bgColor}
/>
))}
</div>
};
export default LanguageSection;

View File

@ -0,0 +1,31 @@
import React from "react";
import { LuGithub, LuExternalLink } from "react-icons/lu";
import ActionLink from "./ActionLink";
const ProjectInfo = ({
title,
description,
githubLink,
liveDemoUrl,
bgColor,
isPreview
}) => {
return <div className="mb-5">
<h3
className={`${
isPreview ? "text-xs" : "text-base"
} font-semibold text-gray-900`}
>
{title}
</h3>
<p className="text-sm text-gray-700 font-medium mt-1">{description}</p>
<div className="flex items-center gap-3 mt-2">
{githubLink && <ActionLink icon={<LuGithub />} link={githubLink} bgColor={bgColor} />}
{liveDemoUrl && <ActionLink icon={<LuExternalLink />} link={liveDemoUrl} bgColor={bgColor} />}
</div>
</div>
};
export default ProjectInfo;

View File

@ -0,0 +1,37 @@
import React from "react";
const RatingInput = ({
value = 0,
total = 5,
onChange = () => {},
color = "#9125E6",
bgColor = "#E9D4FF",
}) => {
// Convert 0100 to 05 scale
const displayValue = Math.round((value / 100) * total);
const handleClick = (index) => {
// Convert 05 scale back to 0100 for DB
const newValue = Math.round(((index + 1) / total) * 100);
onChange(newValue);
};
return <div className="flex gap-3 cursor-pointer">
{[...Array(total)].map((_, index) => {
const isActive = index < displayValue;
return (
<div
key={index}
onClick={() => handleClick(index)}
className="w-4 h-4 rounded transition-all"
style={{
backgroundColor: isActive ? color : bgColor,
}}
></div>
);
})}
</div>
};
export default RatingInput;

View File

@ -0,0 +1,33 @@
import React from "react";
import Progress from "../Progress";
const SkillInfo = ({ skill, progress, accentColor, bgColor }) => {
return (
<div className="flex items-center justify-between">
<p className={`text-[12px] font-semibold text-gray-900`}>{skill}</p>
{progress > 0 && (
<Progress
progress={(progress / 100) * 5}
color={accentColor}
bgColor={bgColor}
/>
)}
</div>
);
};
const SkillSection = ({ skills, accentColor, bgColor }) => {
return <div className="grid grid-cols-2 gap-x-5 gap-y-1 mb-5">
{skills?.map((skill, index) => (
<SkillInfo
key={`skill_${index}`}
skill={skill.name}
progress={skill.progress}
accentColor={accentColor}
bgColor={bgColor}
/>
))}
</div>
};
export default SkillSection;

View File

@ -0,0 +1,30 @@
import React from "react";
const WorkExperience = ({
company,
role,
duration,
durationColor,
description,
}) => {
return <div className="mb-5">
<div className="flex items-start justify-between">
<div>
<h3 className="text-[15px] font-semibold text-gray-900">
{company}
</h3>
<p className="text-[15px] text-gray-700 font-medium">{role}</p>
</div>
<p className="text-xs font-bold italic" style={{color: durationColor}}>
{duration}
</p>
</div>
<p className="text-sm text-gray-600 font-medium italic mt-[0.2cqw]">
{description}
</p>
</div>
};
export default WorkExperience;

View File

@ -0,0 +1,48 @@
import React from "react";
import TemplateOne from "./TemplateOne";
import TemplateTwo from "./TemplateTwo";
import TemplateThree from "./TemplateThree";
const RenderResume = ({
templateId,
resumeData,
colorPalette,
containerWidth,
}) => {
switch (templateId) {
case "01":
return (
<TemplateOne
resumeData={resumeData}
colorPalette={colorPalette}
containerWidth={containerWidth}
/>
);
case "02":
return (
<TemplateTwo
resumeData={resumeData}
colorPalette={colorPalette}
containerWidth={containerWidth}
/>
);
case "03":
return (
<TemplateThree
resumeData={resumeData}
colorPalette={colorPalette}
containerWidth={containerWidth}
/>
);
default:
return (
<TemplateOne
resumeData={resumeData}
colorPalette={colorPalette}
containerWidth={containerWidth}
/>
);
}
};
export default RenderResume;

View File

@ -0,0 +1,254 @@
import React, { useEffect, useRef, useState } from "react";
import {
LuMapPinHouse,
LuMail,
LuPhone,
LuRss,
LuGithub,
LuUser,
} from "react-icons/lu";
import { RiLinkedinLine } from "react-icons/ri";
import ContactInfo from "../ResumeSections/ContactInfo";
import EducationInfo from "../ResumeSections/EducationInfo";
import { formatYearMonth } from "../../utils/helper";
import LanguageSection from "../ResumeSections/LanguageSection";
import WorkExperience from "../ResumeSections/WorkExperience";
import ProjectInfo from "../ResumeSections/ProjectInfo";
import SkillSection from "../ResumeSections/SkillSection";
import CertificationInfo from "../ResumeSections/CertificationInfo";
const DEFAULT_THEME = ["#EBFDFF", "#A1F4FD", "#CEFAFE", "#00B8DB", "#4A5565"];
const Title = ({ text, color }) => {
return (
<div className="relative w-fit mb-2.5">
<span
className="absolute bottom-0 left-0 w-full h-2"
style={{ backgroundColor: color }}
></span>
<h2 className={`relative text-sm font-bold`}>{text}</h2>
</div>
);
};
const TemplateOne = ({ resumeData, colorPalette, containerWidth }) => {
const themeColors = colorPalette?.length > 0 ? colorPalette : DEFAULT_THEME;
const resumeRef = useRef(null);
const [baseWidth, setBaseWidth] = useState(800); // Default value
const [scale, setScale] = useState(1);
useEffect(() => {
// Calculate the scale factor based on the container width
const actualBaseWidth = resumeRef.current.offsetWidth;
setBaseWidth(actualBaseWidth); // Get the actual base width
setScale(containerWidth / baseWidth);
}, [containerWidth]);
return (
<div
ref={resumeRef}
className="p-3 bg-white"
style={{
transform: containerWidth > 0 ? `scale(${scale})` : "none",
transformOrigin: "top left",
width: containerWidth > 0 ? `${baseWidth}px` : "auto", // Keep the original size so scaling works correctly
height: "auto",
}}
>
<div className="grid grid-cols-12 gap-8">
<div
className="col-span-4 py-10"
style={{ backgroundColor: themeColors[0] }}
>
<div className="flex flex-col items-center px-2">
<div
className="w-[100px] h-[100px] max-w-[110px] max-h-[110px] rounded-full flex items-center justify-center"
style={{ backgroundColor: themeColors[1] }}
>
{resumeData.profileInfo.profilePreviewUrl ? (
<img
src={resumeData.profileInfo.profilePreviewUrl}
className="w-[90px] h-[90px] rounded-full"
/>
) : (
<div
className="w-[90px] h-[90px] flex items-center justify-center text-5xl rounded-full"
style={{ color: themeColors[4] }}
>
<LuUser />
</div>
)}
</div>
<h2 className="text-xl font-bold mt-3">
{resumeData.profileInfo.fullName}
</h2>
<p className="text-sm text-center">
{resumeData.profileInfo.designation}
</p>
</div>
<div className="my-6 mx-6">
<div className="flex flex-col gap-4">
<ContactInfo
icon={<LuMapPinHouse />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.location}
/>
<ContactInfo
icon={<LuMail />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.email}
/>
<ContactInfo
icon={<LuPhone />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.phone}
/>
{resumeData.contactInfo.linkedin && (
<ContactInfo
icon={<RiLinkedinLine />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.linkedin}
/>
)}
{resumeData.contactInfo.github && (
<ContactInfo
icon={<LuGithub />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.github}
/>
)}
<ContactInfo
icon={<LuRss />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.website}
/>
</div>
<div className="mt-5">
<Title text="Education" color={themeColors[1]} />
{resumeData.education.map((data, index) => (
<EducationInfo
key={`education_${index}`}
degree={data.degree}
institution={data.institution}
duration={`${formatYearMonth(
data.startDate
)} - ${formatYearMonth(data.endDate)}`}
/>
))}
</div>
<div className="mt-5">
<Title text="Languages" color={themeColors[1]} />
<LanguageSection
languages={resumeData.languages}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
</div>
</div>
<div className="col-span-8 pt-10 mr-10 pb-5">
<div>
<Title text="Professional Summary" color={themeColors[1]} />
<p className="text-sm font-medium">
{resumeData.profileInfo.summary}
</p>
</div>
<div className="mt-4">
<Title text="Work Experiance" color={themeColors[1]} />
{resumeData.workExperience.map((data, index) => (
<WorkExperience
key={`work_${index}`}
company={data.company}
role={data.role}
duration={`${formatYearMonth(
data.startDate
)} - ${formatYearMonth(data.endDate)}`}
durationColor={themeColors[4]}
description={data.description}
/>
))}
</div>
<div className="mt-4">
<Title text="Projects" color={themeColors[1]} />
{resumeData.projects.map((project, index) => (
<ProjectInfo
key={`project_${index}`}
title={project.title}
description={project.description}
githubLink={project.github}
liveDemoUrl={project.liveDemo}
bgColor={themeColors[2]}
/>
))}
</div>
<div className="mt-4">
<Title text="Skills" color={themeColors[1]} />
<SkillSection
skills={resumeData.skills}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
<div className="mt-4">
<Title text="Certifications" color={themeColors[1]} />
<div className="grid grid-cols-2 gap-2">
{resumeData.certifications.map((data, index) => (
<CertificationInfo
key={`cert_${index}`}
title={data.title}
issuer={data.issuer}
year={data.year}
bgColor={themeColors[2]}
/>
))}
</div>
</div>
{resumeData.interests.length > 0 && resumeData.interests[0] != "" && (
<div className="mt-4">
<Title text="Interests" color={themeColors[1]} />
<div className="flex items-center flex-wrap gap-3 mt-4">
{resumeData.interests.map((interest, index) => {
if (!interest) return null;
return (
<div
key={`interest_${index}`}
className="text-[10px] font-medium py-1 px-3 rounded-lg"
style={{ backgroundColor: themeColors[2] }}
>
{interest}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TemplateOne;

View File

@ -0,0 +1,264 @@
import React, { useEffect, useRef, useState } from "react";
import {
LuMapPinHouse,
LuMail,
LuPhone,
LuRss,
LuGithub,
LuUser,
} from "react-icons/lu";
import { RiLinkedinLine } from "react-icons/ri";
import ContactInfo from "../ResumeSections/ContactInfo";
import EducationInfo from "../ResumeSections/EducationInfo";
import { formatYearMonth } from "../../utils/helper";
import LanguageSection from "../ResumeSections/LanguageSection";
import WorkExperience from "../ResumeSections/WorkExperience";
import ProjectInfo from "../ResumeSections/ProjectInfo";
import SkillSection from "../ResumeSections/SkillSection";
import CertificationInfo from "../ResumeSections/CertificationInfo";
const DEFAULT_THEME = ["#EBFDFF", "#A1F4FD", "#CEFAFE", "#00B8DB", "#4A5565"];
const Title = ({ text, color }) => {
return (
<div className="relative w-fit mb-2.5">
<span
className="absolute bottom-0 left-0 w-full h-2"
style={{ backgroundColor: color }}
></span>
<h2 className={`relative text-sm font-bold`}>{text}</h2>
</div>
);
};
const TemplateThree = ({ resumeData, colorPalette, containerWidth }) => {
const themeColors = colorPalette?.length > 0 ? colorPalette : DEFAULT_THEME;
const resumeRef = useRef(null);
const [baseWidth, setBaseWidth] = useState(800); // Default value
const [scale, setScale] = useState(1);
useEffect(() => {
// Calculate the scale factor based on the container width
const actualBaseWidth = resumeRef.current.offsetWidth;
setBaseWidth(actualBaseWidth); // Get the actual base width
setScale(containerWidth / baseWidth);
}, [containerWidth]);
return (
<div
ref={resumeRef}
className="p-3 bg-white"
style={{
transform: containerWidth > 0 ? `scale(${scale})` : "none",
transformOrigin: "top left",
width: containerWidth > 0 ? `${baseWidth}px` : "auto", // Keep the original size so scaling works correctly
height: "auto",
}}
>
<div className="flex items-start gap-5 px-2 mb-5">
<div
className="w-[100px] h-[100px] max-w-[105px] max-h-[105px] rounded-2xl flex items-center justify-center"
style={{ backgroundColor: themeColors[1] }}
>
{resumeData.profileInfo.profilePreviewUrl ? (
<img
src={resumeData.profileInfo.profilePreviewUrl}
className="w-[90px] h-[90px] rounded-2xl"
/>
) : (
<div
className="w-[90px] h-[90px] flex items-center justify-center text-5xl rounded-full"
style={{ color: themeColors[4] }}
>
<LuUser />
</div>
)}
</div>
<div>
<div className="grid grid-cols-12 items-center">
<div className="col-span-8">
<h2 className="text-2xl font-bold">
{resumeData.profileInfo.fullName}
</h2>
<p className="text-[15px] font-semibold mb-2">
{resumeData.profileInfo.designation}
</p>
<ContactInfo
icon={<LuMapPinHouse />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.location}
/>
</div>
<div className="col-span-4 flex flex-col gap-5 mt-2">
<ContactInfo
icon={<LuMail />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.email}
/>
<ContactInfo
icon={<LuPhone />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.phone}
/>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-8">
<div
className="col-span-4 py-10"
style={{ backgroundColor: themeColors[0] }}
>
<div className="my-6 mx-6">
<div className="flex flex-col gap-4">
{resumeData.contactInfo.linkedin && (
<ContactInfo
icon={<RiLinkedinLine />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.linkedin}
/>
)}
{resumeData.contactInfo.github && (
<ContactInfo
icon={<LuGithub />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.github}
/>
)}
<ContactInfo
icon={<LuRss />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.website}
/>
</div>
<div className="mt-5">
<Title text="Education" color={themeColors[1]} />
{resumeData.education.map((data, index) => (
<EducationInfo
key={`education_${index}`}
degree={data.degree}
institution={data.institution}
duration={`${formatYearMonth(
data.startDate
)} - ${formatYearMonth(data.endDate)}`}
/>
))}
</div>
<div className="mt-5">
<Title text="Languages" color={themeColors[1]} />
<LanguageSection
languages={resumeData.languages}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
</div>
</div>
<div className="col-span-8 pt-10 mr-10 pb-5">
<div>
<Title text="Professional Summary" color={themeColors[1]} />
<p className="text-sm font-medium">
{resumeData.profileInfo.summary}
</p>
</div>
<div className="mt-4">
<Title text="Work Experiance" color={themeColors[1]} />
{resumeData.workExperience.map((data, index) => (
<WorkExperience
key={`work_${index}`}
company={data.company}
role={data.role}
duration={`${formatYearMonth(
data.startDate
)} - ${formatYearMonth(data.endDate)}`}
durationColor={themeColors[4]}
description={data.description}
/>
))}
</div>
<div className="mt-4">
<Title text="Projects" color={themeColors[1]} />
{resumeData.projects.map((project, index) => (
<ProjectInfo
key={`project_${index}`}
title={project.title}
description={project.description}
githubLink={project.github}
liveDemoUrl={project.liveDemo}
bgColor={themeColors[2]}
/>
))}
</div>
<div className="mt-4">
<Title text="Skills" color={themeColors[1]} />
<SkillSection
skills={resumeData.skills}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
<div className="mt-4">
<Title text="Certifications" color={themeColors[1]} />
<div className="grid grid-cols-2 gap-2">
{resumeData.certifications.map((data, index) => (
<CertificationInfo
key={`cert_${index}`}
title={data.title}
issuer={data.issuer}
year={data.year}
bgColor={themeColors[2]}
/>
))}
</div>
</div>
{resumeData.interests.length > 0 && resumeData.interests[0] != "" && (
<div className="mt-4">
<Title text="Interests" color={themeColors[1]} />
<div className="flex items-center flex-wrap gap-3 mt-4">
{resumeData.interests.map((interest, index) => {
if (!interest) return null;
return (
<div
key={`interest_${index}`}
className="text-[10px] font-medium py-1 px-3 rounded-lg"
style={{ backgroundColor: themeColors[2] }}
>
{interest}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TemplateThree;

View File

@ -0,0 +1,254 @@
import React, { useEffect, useRef, useState } from "react";
import {
LuMapPinHouse,
LuMail,
LuPhone,
LuRss,
LuGithub,
LuUser,
} from "react-icons/lu";
import { RiLinkedinLine } from "react-icons/ri";
import ContactInfo from "../ResumeSections/ContactInfo";
import EducationInfo from "../ResumeSections/EducationInfo";
import { formatYearMonth } from "../../utils/helper";
import LanguageSection from "../ResumeSections/LanguageSection";
import WorkExperience from "../ResumeSections/WorkExperience";
import ProjectInfo from "../ResumeSections/ProjectInfo";
import SkillSection from "../ResumeSections/SkillSection";
import CertificationInfo from "../ResumeSections/CertificationInfo";
const DEFAULT_THEME = ["#EBFDFF", "#A1F4FD", "#CEFAFE", "#00B8DB", "#4A5565"];
const Title = ({ text, color }) => {
return (
<div className="relative w-fit mb-2.5">
<span
className="absolute bottom-0 left-0 w-full h-2"
style={{ backgroundColor: color }}
></span>
<h2 className={`relative text-sm font-bold`}>{text}</h2>
</div>
);
};
const TemplateTwo = ({ resumeData, colorPalette, containerWidth }) => {
const themeColors = colorPalette?.length > 0 ? colorPalette : DEFAULT_THEME;
const resumeRef = useRef(null);
const [baseWidth, setBaseWidth] = useState(800); // Default value
const [scale, setScale] = useState(1);
useEffect(() => {
// Calculate the scale factor based on the container width
const actualBaseWidth = resumeRef.current.offsetWidth;
setBaseWidth(actualBaseWidth); // Get the actual base width
setScale(containerWidth / baseWidth);
}, [containerWidth]);
return (
<div
ref={resumeRef}
className="p-3 bg-white"
style={{
transform: containerWidth > 0 ? `scale(${scale})` : "none",
transformOrigin: "top left",
width: containerWidth > 0 ? `${baseWidth}px` : "auto", // Keep the original size so scaling works correctly
height: "auto",
}}
>
<div className="px-10 pt-10 pb-5">
<div className="flex items-start gap-5 mb-5">
<div
className="w-[140px] h-[140px] max-w-[140px] max-h-[140px] rounded-2xl flex items-center justify-center"
style={{ backgroundColor: themeColors[1] }}
>
{resumeData.profileInfo.profilePreviewUrl ? (
<img
src={resumeData.profileInfo.profilePreviewUrl}
className="w-[140px] h-[140px] rounded-2xl"
/>
) : (
<div
className="w-[140px] h-[140px] flex items-center justify-center text-5xl rounded-full"
style={{ color: themeColors[4] }}
>
<LuUser />
</div>
)}
</div>
<div>
<div className="grid grid-cols-12 gap-2 items-center">
<div className="col-span-6">
<h2 className="text-2xl font-bold">
{resumeData.profileInfo.fullName}
</h2>
<p className="text-[15px] font-semibold mb-2">
{resumeData.profileInfo.designation}
</p>
<ContactInfo
icon={<LuMapPinHouse />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.location}
/>
</div>
<div className="col-span-6 flex flex-col gap-5 mt-2">
<ContactInfo
icon={<LuMail />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.email}
/>
<ContactInfo
icon={<LuPhone />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.phone}
/>
</div>
<div className="col-span-6">
{resumeData.contactInfo.linkedin && (
<ContactInfo
icon={<RiLinkedinLine />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.linkedin}
/>
)}
</div>
<div className="col-span-6">
<ContactInfo
icon={<LuRss />}
iconBG={themeColors[2]}
value={resumeData.contactInfo.website}
/>
</div>
</div>
</div>
</div>
</div>
<div className="mx-10 pb-5">
<div>
<Title text="Professional Summary" color={themeColors[1]} />
<p className="text-sm font-medium">
{resumeData.profileInfo.summary}
</p>
</div>
<div className="mt-4">
<Title text="Work Experiance" color={themeColors[1]} />
{resumeData.workExperience.map((data, index) => (
<WorkExperience
key={`work_${index}`}
company={data.company}
role={data.role}
duration={`${formatYearMonth(data.startDate)} - ${formatYearMonth(
data.endDate
)}`}
durationColor={themeColors[4]}
description={data.description}
/>
))}
</div>
<div className="mt-4">
<Title text="Projects" color={themeColors[1]} />
{resumeData.projects.map((project, index) => (
<ProjectInfo
key={`project_${index}`}
title={project.title}
description={project.description}
githubLink={project.github}
liveDemoUrl={project.liveDemo}
bgColor={themeColors[2]}
/>
))}
</div>
<div className="mt-5">
<Title text="Education" color={themeColors[1]} />
<div className="grid grid-cols-2 gap-3">
{resumeData.education.map((data, index) => (
<EducationInfo
key={`education_${index}`}
degree={data.degree}
institution={data.institution}
duration={`${formatYearMonth(
data.startDate
)} - ${formatYearMonth(data.endDate)}`}
/>
))}
</div>
</div>
<div className="mt-4">
<Title text="Certifications" color={themeColors[1]} />
<div className="grid grid-cols-2 gap-6">
{resumeData.certifications.map((data, index) => (
<CertificationInfo
key={`cert_${index}`}
title={data.title}
issuer={data.issuer}
year={data.year}
bgColor={themeColors[2]}
/>
))}
</div>
</div>
<div className="mt-4">
<Title text="Skills" color={themeColors[1]} />
<SkillSection
skills={resumeData.skills}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
<div className="grid grid-cols-2 gap-10 mt-4">
<div className="">
<Title text="Languages" color={themeColors[1]} />
<LanguageSection
languages={resumeData.languages}
accentColor={themeColors[3]}
bgColor={themeColors[2]}
/>
</div>
{resumeData.interests.length > 0 && resumeData.interests[0] != "" && (
<div className="">
<Title text="Interests" color={themeColors[1]} />
<div className="flex items-center flex-wrap gap-3 mt-4">
{resumeData.interests.map((interest, index) => {
if (!interest) return null;
return (
<div
key={`interest_${index}`}
className="text-[10px] font-medium py-1 px-3 rounded-lg"
style={{ backgroundColor: themeColors[2] }}
>
{interest}
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default TemplateTwo;

View File

@ -0,0 +1,14 @@
import React from 'react'
const StepProgress = ({progress}) => {
return (
<div className="w-full bg-purple-50 h-1 overflow-hidden rounded-[2px]">
<div
className="h-1 bg-linear-to-r from-purple-500/85 to-purple-700 transition-all rounded"
style={{ width: `${progress}%` }}
></div>
</div>
)
}
export default StepProgress

View File

@ -0,0 +1,31 @@
import React from 'react'
const Tabs = ({tabs, activeTab, setActiveTab}) => {
return (
<div className="my-2">
<div className="flex">
{tabs.map((tab) => (
<button
key={tab.label}
className={`relative px-3 md:px-4 py-2 text-sm font-medium ${
activeTab === tab.label
? "text-primary"
: "text-gray-500 hover:text-gray-700"
} cursor-pointer`}
onClick={() => setActiveTab(tab.label)}
>
<div className="flex items-center">
<span className="text-[14px] font-semibold text-purple-700">{tab.label}</span>
</div>
{activeTab === tab.label && (
<div className="absolute bottom-0 left-0 w-full h-0.5 bg-linear-to-r from-purple-500/85 to-purple-700"></div>
)}
</button>
))}
</div>
</div>
)
}
export default Tabs

View File

@ -0,0 +1,16 @@
import React, { useContext } from "react";
import { UserContext } from "../../context/userContext";
import Navbar from "./Navbar";
const DashboardLayout = ({ activeMenu, children }) => {
const { user } = useContext(UserContext);
return (
<div>
<Navbar activeMenu={activeMenu} />
{user && <div className="container mx-auto pt-4 pb-4">{children}</div>}
</div>
);
};
export default DashboardLayout;

View File

@ -0,0 +1,19 @@
import React from "react";
import ProfileInfoCard from "../Cards/ProfileInfoCard";
import { Link } from "react-router-dom";
const Navbar = () => {
return <div className="h-16 bg-white border boredr-b border-gray-200/50 backdrop-blur-[2px] py-2.5 px-4 md:px-0 sticky top-0 z-30">
<div className="container mx-auto flex items-center justify-between gap-5">
<Link to='/dashboard'>
<h2 className="text-lg md:text-xl font-medium text-black leading-5">
Resume Builder
</h2>
</Link>
<ProfileInfoCard />
</div>
</div>
};
export default Navbar;

View File

@ -0,0 +1,53 @@
import React, { createContext, useState, useEffect } from "react";
import axiosInstance from "../utils/axiosInstance";
import { API_PATHS } from "../utils/apiPaths";
export const UserContext = createContext();
const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // New state to track loading
useEffect(() => {
if (user) return;
const accessToken = localStorage.getItem("token");
if (!accessToken) {
setLoading(false);
return;
}
const fetchUser = async () => {
try {
const response = await axiosInstance.get(API_PATHS.AUTH.GET_PROFILE);
setUser(response.data);
} catch (error) {
console.error("User not authenticated", error);
clearUser();
} finally {
setLoading(false);
}
};
fetchUser();
}, []);
const updateUser = (userData) => {
setUser(userData);
localStorage.setItem("token", userData.token); // Save token
setLoading(false);
};
const clearUser = () => {
setUser(null);
localStorage.removeItem("token");
};
return (
<UserContext.Provider value={{ user, loading, updateUser, clearUser }}>
{children}
</UserContext.Provider>
);
};
export default UserProvider;

View File

@ -0,0 +1,69 @@
@import url('https://fonts.googleapis.com/css2?family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap');
@import "tailwindcss";
@theme {
--font-display: "Urbanist", sans-serif;
--breakpoint-3xl: 1920px;
--color-primary: #9328E7;
}
@layer base {
html {
font-family: var(--font-display);
}
body {
background-color: #fcfbfc;
overflow-x: hidden;
}
}
/* Custom scrollbar styling */
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(100, 100, 100, 0.4);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
@layer utilities {
/* Animate text with a shine effect */
@keyframes text-shine {
0% {
background-position: 0% 50%;
}
100% {
background-position: 100% 50%;
}
}
.animate-text-shine {
animation: text-shine 3s ease-in-out infinite alternate;
}
}
.input-box {
@apply w-full flex justify-between gap-3 text-sm text-black bg-gray-50/50 rounded px-4 py-3 mb-4 mt-3 border border-gray-100 outline-none focus-within:border-purple-300;
}
.btn-primary {
@apply w-full text-sm font-medium text-white bg-black shadow-lg shadow-purple-600/5 p-[10px] rounded-md my-1 hover:bg-purple-600/15 hover:text-black cursor-pointer;
}
.btn-small {
@apply flex items-center gap-2 text-[13px] font-semibold text-white bg-linear-to-r from-purple-500/85 to-purple-700 px-5 py-1.5 rounded cursor-pointer;
}
.btn-small-light {
@apply flex items-center gap-2 text-[12px] font-semibold text-purple-800 bg-purple-600/15 border border-purple-50 hover:border-purple-400 px-3 py-1.5 rounded cursor-pointer;
}
.form-input {
@apply w-full text-sm text-black outline-none bg-white border border-slate-100 px-2.5 py-3 rounded-md mt-2 placeholder:text-gray-500 focus-within:border-purple-300;
}

View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,102 @@
import React, { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import Input from "../../components/Inputs/Input";
import { validateEmail } from "../../utils/helper";
import { UserContext } from "../../context/userContext";
import axiosInstance from "../../utils/axiosInstance";
import { API_PATHS } from "../../utils/apiPaths";
const Login = ({ setCurrentPage }) => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const { updateUser } = useContext(UserContext);
const navigate = useNavigate();
// Handle Login Form Submit
const handleLogin = async (e) => {
e.preventDefault();
if (!validateEmail(email)) {
setError("Please enter a valid email address.");
return;
}
if (!password) {
setError("Please enter the password");
return;
}
setError("");
//Login API Call
try {
const response = await axiosInstance.post(API_PATHS.AUTH.LOGIN, {
email,
password,
});
const { token } = response.data;
if (token) {
localStorage.setItem("token", token);
updateUser(response.data);
navigate("/dashboard");
}
} catch (error) {
if (error.response && error.response.data.message) {
setError(error.response.data.message);
} else {
setError("Something went wrong. Please try again.");
}
}
};
return (
<div className="w-[90vw] md:w-[33vw] p-7 flex flex-col justify-center">
<h3 className="text-lg font-semibold text-black">Welcome Back</h3>
<p className="text-xs text-slate-700 mt-[5px] mb-6">
Please enter your details to log in
</p>
<form onSubmit={handleLogin}>
<Input
value={email}
onChange={({ target }) => setEmail(target.value)}
label="Email Address"
placeholder="john@example.com"
type="text"
/>
<Input
value={password}
onChange={({ target }) => setPassword(target.value)}
label="Password"
placeholder="Min 8 Characters"
type="password"
/>
{error && <p className="text-red-500 text-xs pb-2.5">{error}</p>}
<button type="submit" className="btn-primary">
LOGIN
</button>
<p className="text-[13px] text-slate-800 mt-3">
Dont have an account?{" "}
<button
className="font-medium text-primary underline cursor-pointer"
onClick={() => {
setCurrentPage("signup");
}}
>
SignUp
</button>
</p>
</form>
</div>
);
};
export default Login;

View File

@ -0,0 +1,135 @@
import React, { useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import Input from "../../components/Inputs/Input";
import { validateEmail } from "../../utils/helper";
import ProfilePhotoSelector from "../../components/Inputs/ProfilePhotoSelector";
import axiosInstance from "../../utils/axiosInstance";
import { API_PATHS } from "../../utils/apiPaths";
import { UserContext } from "../../context/userContext";
import uploadImage from "../../utils/uploadImage";
const SignUp = ({setCurrentPage}) => {
const [profilePic, setProfilePic] = useState(null);
const [fullName, setFullName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const { updateUser } = useContext(UserContext);
const navigate = useNavigate();
// Handle SignUp Form Submit
const handleSignUp = async (e) => {
e.preventDefault();
let profileImageUrl = "";
if (!fullName) {
setError("Please enter full name.");
return;
}
if (!validateEmail(email)) {
setError("Please enter a valid email address.");
return;
}
if (!password) {
setError("Please enter the password");
return;
}
setError("");
//SignUp API Call
try {
// Upload image if present
if (profilePic) {
const imgUploadRes = await uploadImage(profilePic);
profileImageUrl = imgUploadRes.imageUrl || "";
}
const response = await axiosInstance.post(API_PATHS.AUTH.REGISTER, {
name: fullName,
email,
password,
profileImageUrl,
});
const { token } = response.data;
if (token) {
localStorage.setItem("token", token);
updateUser(response.data);
navigate("/dashboard");
}
} catch (error) {
if (error.response && error.response.data.message) {
setError(error.response.data.message);
} else {
setError("Something went wrong. Please try again.");
}
}
};
return (
<div className="w-[90vw] md:w-[33vw] p-7 flex flex-col justify-center">
<h3 className="text-lg font-semibold text-black">Create an Account</h3>
<p className="text-xs text-slate-700 mt-[5px] mb-6">
Join us today by entering your details below.
</p>
<form onSubmit={handleSignUp}>
<ProfilePhotoSelector image={profilePic} setImage={setProfilePic} />
<div className="grid grid-cols-1 md:grid-cols-1 gap-2">
<Input
value={fullName}
onChange={({ target }) => setFullName(target.value)}
label="Full Name"
placeholder="John"
type="text"
/>
<Input
value={email}
onChange={({ target }) => setEmail(target.value)}
label="Email Address"
placeholder="john@example.com"
type="text"
/>
<Input
value={password}
onChange={({ target }) => setPassword(target.value)}
label="Password"
placeholder="Min 8 Characters"
type="password"
/>
</div>
{error && <p className="text-red-500 text-xs pb-2.5">{error}</p>}
<button type="submit" className="btn-primary">
SIGN UP
</button>
<p className="text-[13px] text-slate-800 mt-3">
Already an account?{" "}
<button
className="font-medium text-primary underline cursor-pointer"
onClick={() => {
setCurrentPage("login");
}}
>
Login
</button>
</p>
</form>
</div>
)
}
export default SignUp

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom';
import Input from "../../components/Inputs/Input";
import axiosInstance from '../../utils/axiosInstance';
import { API_PATHS } from '../../utils/apiPaths';
const CreateResumeForm = () => {
const [title, setTitle] = useState("");
const [error, setError] = useState(null);
const navigate = useNavigate();
// Handle Create Resume
const handleCreateResume = async (e) => {
e.preventDefault();
if (!title) {
setError("Please resume title");
return;
}
setError("");
//Create Resume API Call
try {
const response = await axiosInstance.post(API_PATHS.RESUME.CREATE, {
title,
});
if (response.data?._id) {
navigate(`/resume/${response.data?._id}`);
}
} catch (error) {
if (error.response && error.response.data.message) {
setError(error.response.data.message);
} else {
setError("Something went wrong. Please try again.");
}
}
};
return (
<div className="w-[90vw] md:w-[70vh] p-7 flex flex-col justify-center">
<h3 className="text-lg font-semibold text-black">Create New Resume</h3>
<p className="text-xs text-slate-700 mt-[5px] mb-3">
Give your resume a title to get started. You can edit all details later.
</p>
<form onSubmit={handleCreateResume}>
<Input
value={title}
onChange={({ target }) => setTitle(target.value)}
label="Resume Title"
placeholder="Eg: Mike's Resume"
type="text"
/>
{error && <p className="text-red-500 text-xs pb-2.5">{error}</p>}
<button type="submit" className="btn-primary">
Create Resume
</button>
</form>
</div>
)
}
export default CreateResumeForm

View File

@ -0,0 +1,73 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import axiosInstance from "../../utils/axiosInstance";
import { API_PATHS } from "../../utils/apiPaths";
import DashboardLayout from "../../components/layouts/DashboardLayout";
import {LuCirclePlus} from 'react-icons/lu'
import moment from 'moment'
import ResumeSummaryCard from "../../components/Cards/ResumeSummaryCard";
import CreateResumeForm from "./CreateResumeForm";
import Modal from "../../components/Modal";
const Dashboard = () => {
const navigate = useNavigate();
const [openCreateModal, setOpenCreateModal] = useState(false);
const [allResumes, setAllResumes] = useState(null);
const fetchAllResumes = async () => {
try {
const response = await axiosInstance.get(API_PATHS.RESUME.GET_ALL);
setAllResumes(response.data);
} catch (error) {
console.error("Error fetching resumes:", error);
}
};
useEffect(() => {
fetchAllResumes();
}, []);
return <DashboardLayout>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 md:gap-7 pt-1 pb-6 px-4 md:px-0">
<div
className="h-[300px] flex flex-col gap-5 items-center justify-center bg-white rounded-lg border border-purple-100 hover:border-purple-300 hover:bg-purple-50/5 cursor-pointer"
onClick={() => setOpenCreateModal(true)}
>
<div className="w-12 h-12 flex items-center justify-center bg-purple-200/60 rounded-2xl">
<LuCirclePlus className="text-xl text-purple-500" />
</div>
<h3 className="font-medium text-gray-800">Add New Resume</h3>
</div>
{allResumes?.map((resume) => (
<ResumeSummaryCard
key={resume?._id}
imgUrl={resume?.thumbnailLink || null}
title={resume.title}
lastUpdated={
resume?.updatedAt
? moment(resume.updatedAt).format("Do MMM YYYY")
: ""
}
onSelect={()=>navigate(`/resume/${resume?._id}`)}
/>
))}
</div>
<Modal
isOpen={openCreateModal}
onClose={() => {
setOpenCreateModal(false);
}}
hideHeader
>
<div>
<CreateResumeForm />
</div>
</Modal>
</DashboardLayout>;
};
export default Dashboard;

View File

@ -0,0 +1,130 @@
import React, { useContext, useState } from "react";
import HERO_IMG from "../assets/hero-img.png";
import { useNavigate } from "react-router-dom";
import Login from "./Auth/Login";
import SignUp from "./Auth/SignUp";
import Modal from "../components/Modal";
import { UserContext } from "../context/userContext";
import ProfileInfoCard from "../components/Cards/ProfileInfoCard";
const LandingPage = () => {
const { user } = useContext(UserContext);
const navigate = useNavigate();
const [openAuthModal, setOpenAuthModal] = useState(false);
const [currentPage, setCurrentPage] = useState("login");
const handleCTA = () => {
if (!user) {
setOpenAuthModal(true);
} else {
navigate("/dashboard");
}
};
return (
<div className="w-full min-h-full bg-white">
<div className="container mx-auto px-4 py-6">
{/* Header */}
<header className="flex justify-between items-center mb-16">
<div className="text-xl font-bold">Resume Builder</div>
{user ? (
<ProfileInfoCard />
) : (
<button
className="bg-purple-100 text-sm font-semibold text-black px-7 py-2.5 rounded-lg hover:bg-gray-800 hover:text-white transition-colors cursor-pointer"
onClick={() => setOpenAuthModal(true)}
>
Login / Sign Up
</button>
)}
</header>
{/* Hero Content */}
<div className="flex flex-col md:flex-row items-center">
<div className="w-full md:w-1/2 pr-4 mb-8 md:mb-0">
<h1 className="text-5xl font-bold mb-6 leading-tight">
Build Your{" "}
<span className="text-transparent bg-clip-text bg-[radial-gradient(circle,_#7182ff_0%,_#3cff52_100%)] bg-[length:200%_200%] animate-text-shine">
Resume Effortlessly
</span>
</h1>
<p className="text-lg text-gray-700 mb-8">
Craft a standout resume in minutes with our smart and intuitive
resume builder.
</p>
<button
className="bg-black text-sm font-semibold text-white px-8 py-3 rounded-lg hover:bg-gray-800 transition-colors cursor-pointer"
onClick={handleCTA}
>
Get Started
</button>
</div>
<div className="w-full md:w-1/2">
<img
src={HERO_IMG}
alt="Hero Image"
className="w-full rounded-lg"
/>
</div>
</div>
<section className="mt-5">
<h2 className="text-2xl font-bold text-center mb-12">
Features That Make You Shine
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-gray-50 p-6 rounded-xl shadow-sm hover:shadow-md transition">
<h3 className="text-lg font-semibold mb-3">Easy Editing</h3>
<p className="text-gray-600">
Update your resume sections with live preview and instant
formatting.
</p>
</div>
<div className="bg-gray-50 p-6 rounded-xl shadow-sm hover:shadow-md transition">
<h3 className="text-lg font-semibold mb-3">
Beautiful Templates
</h3>
<p className="text-gray-600">
Choose from modern, professional templates that are easy to
customize.
</p>
</div>
<div className="bg-gray-50 p-6 rounded-xl shadow-sm hover:shadow-md transition">
<h3 className="text-lg font-semibold mb-3">One-Click Export</h3>
<p className="text-gray-600">
Download your resume instantly as a high-quality PDF with one
click.
</p>
</div>
</div>
</section>
</div>
<div className="text-sm bg-gray-50 text-secondary text-center p-5 mt-5">
Made with ... Happy Coding
</div>
<Modal
isOpen={openAuthModal}
onClose={() => {
setOpenAuthModal(false);
setCurrentPage("login");
}}
hideHeader
>
<div>
{currentPage === "login" && <Login setCurrentPage={setCurrentPage} />}
{currentPage === "signup" && (
<SignUp setCurrentPage={setCurrentPage} />
)}
</div>
</Modal>
</div>
);
};
export default LandingPage;

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

View File

@ -0,0 +1,23 @@
export const BASE_URL = "http://localhost:8000";
// utils/apiPaths.js
export const API_PATHS = {
AUTH: {
REGISTER: "/api/auth/register", // Signup
LOGIN: "/api/auth/login", // Authenticate user & return JWT token
GET_PROFILE: "/api/auth/profile", // Get logged-in user details
},
RESUME: {
CREATE: "/api/resume", // POST - Create a new resume
GET_ALL: "/api/resume", // GET - Get all resumes of logged-in user
GET_BY_ID: (id) => `/api/resume/${id}`, // GET - Get a specific resume
UPDATE: (id) => `/api/resume/${id}`, // PUT - Update a resume
DELETE: (id) => `/api/resume/${id}`, // DELETE - Delete a resume
UPLOAD_IMAGES: (id) => `/api/resume/${id}/upload-images`, // PUT - Upload Thumbnail and Resume profile img
},
IMAGE: {
UPLOAD_IMAGE: "api/auth/upload-image",
},
};

View File

@ -0,0 +1,48 @@
import axios from "axios";
import { BASE_URL } from "./apiPaths";
const axiosInstance = axios.create({
baseURL: BASE_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// Request Interceptor
axiosInstance.interceptors.request.use(
(config) => {
const accessToken = localStorage.getItem("token");
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response Interceptor
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
// Handle common errors globally
if (error.response) {
if (error.response.status === 401) {
// Redirect to login page
window.location.href = "/";
} else if (error.response.status === 500) {
console.error("Server error. Please try again later.");
}
} else if (error.code === "ECONNABORTED") {
console.error("Request timeout. Please try again.");
}
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@ -0,0 +1,153 @@
import TEMPLATE_ONE_IMG from '../assets/template-one.png'
import TEMPLATE_TWO_IMG from '../assets/template-two.png'
import TEMPLATE_THREE_IMG from '../assets/template-three.png'
export const resumeTemplates = [
{
id:'01',
thumbnailImg: TEMPLATE_ONE_IMG,
colorPaletteCode: 'themeOne'
},
{
id:'02',
thumbnailImg: TEMPLATE_TWO_IMG,
colorPaletteCode: 'themeTwo'
},
{
id:'03',
thumbnailImg: TEMPLATE_THREE_IMG,
colorPaletteCode: 'themeThree'
},
]
export const themeColorPalette = {
themeOne: [
["#EBFDFF", "#A1F4FD", "#CEFAFE", "#00B8DB", "#4A5565"],
["#E9FBF8", "#B4EFE7", "#93E2DA", "#2AC9A0", "#3D4C5A"],
["#F5F4FF", "#E0DBFF", "#C9C2F8", "#8579D1", "#4B4B5C"],
["#F0FAFF", "#D6F0FF", "#AFDEFF", "#3399FF", "#445361"],
["#FFF5F7", "#FFE0EC", "#FAC6D4", "#F6729C", "#5A5A5A"],
["#F9FAFB", "#E4E7EB", "#CBD5E0", "#7F9CF5", "#2D3748"],
["#F4FFFD", "#D3FDF2", "#B0E9D4", "#34C79D", "#384C48"],
["#FFF7F0", "#FFE6D9", "#FFD2BA", "#FF9561", "#4C4743"],
["#F9FCFF", "#E3F0F9", "#C0DDEE", "#6CA6CF", "#46545E"],
["#FFFDF6", "#FFF4D7", "#FFE7A0", "#FFD000", "#57534E"],
["#EFFCFF", "#C8F0FF", "#99E0FF", "#007BA7", "#2B3A42"],
["#F7F7F7", "#E4E4E4", "#CFCFCF", "#4A4A4A", "#222222"],
["#E3F2FD", "#90CAF9", "#a8d2f4", "#1E88E5", "#0D47A1"],
],
};
export const DUMMY_RESUME_DATA = {
profileInfo: {
profileImg: null,
previewUrl: "",
fullName: "John Doe",
designation: "Senior Software Engineer",
summary:
"Passionate and results-driven developer with 6+ years of experience building full-stack web applications using modern technologies like React, Node.js, and MongoDB.",
},
contactInfo: {
email: "john.doe@example.com",
phone: "+1234567890",
location:'#12 Anywhere, Any City, Any Country',
linkedin: "https://linkedin.com/timetoprogram",
github: "https://github.com/timetoprogram",
website: "https://timetoprogram.com",
},
workExperience: [
{
company: "Tech Solutions",
role: "Senior Frontend Engineer",
startDate: "2022-03",
endDate: "2025-04",
description:
"Leading the frontend team to build scalable enterprise applications using React, Tailwind CSS, and TypeScript.",
},
{
company: "Coding Dev",
role: "Full Stack Developer",
startDate: "2020-01",
endDate: "2022-02",
description:
"Worked on cross-functional teams developing full-stack solutions with React, Node.js, and MongoDB. Improved performance by 30% through code optimization.",
},
{
company: "Startup Company",
role: "Junior Web Developer",
startDate: "2018-06",
endDate: "2019-12",
description:
"Built responsive websites for startups and small businesses. Maintained legacy code and collaborated with designers to enhance UX/UI.",
},
],
education: [
{
degree: "M.Sc. Software Engineering",
institution: "Tech University",
startDate: "2021-08",
endDate: "2023-06",
},
{
degree: "B.Sc. Computer Science",
institution: "State University",
startDate: "2017-08",
endDate: "2021-05",
},
{
degree: "High School Diploma",
institution: "Central High School",
startDate: "2015-06",
endDate: "2017-05",
},
],
skills: [
{ name: "JavaScript", progress: 95 },
{ name: "React", progress: 90 },
{ name: "Node.js", progress: 85 },
{ name: "TypeScript", progress: 80 },
{ name: "MongoDB", progress: 75 },
],
projects: [
{
title: "Project Manager App",
description:
"A task and team management app built with MERN stack. Includes user roles, real-time notifications, and drag-and-drop task boards.",
github: "https://github.com/timetoprogram/project-manager-app",
},
{
title: "E-Commerce Platform",
description:
"An e-commerce site built with Next.js and Stripe integration. Supports cart, payments, order history, and admin dashboard.",
liveDemo: "https://ecommerce-demo.timetoprogram.com",
},
{
title: "Blog CMS",
description:
"A custom CMS for blogging using Express and React. Includes WYSIWYG editor, markdown support, and user management.",
github: "https://github.com/timetoprogram/blog-cms",
liveDemo: "https://blogcms.timetoprogram.dev",
},
],
certifications: [
{
title: "Full Stack Web Developer",
issuer: "Udemy",
year: "2023",
},
{
title: "React Advanced Certification",
issuer: "Coursera",
year: "2022",
},
],
languages: [
{ name: "English", progress: 100 },
{ name: "Spanish", progress: 70 },
{ name: "French", progress: 40 },
],
interests: ["Reading", "Open Source Contribution", "Hiking"],
};

View File

@ -0,0 +1,111 @@
import moment from 'moment'
import html2canvas from "html2canvas";
export const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
// get lightest average color
export const getLightColorFromImage = (imageUrl) => {
return new Promise((resolve, reject) => {
// Check if imageUrl is valid
if (!imageUrl || typeof imageUrl !== 'string') {
return reject(new Error('Invalid image URL'));
}
const img = new Image();
// If not a base64 string, set crossOrigin (important for CORS)
if (!imageUrl.startsWith('data:')) {
img.crossOrigin = 'anonymous';
}
img.src = imageUrl;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
let r = 0, g = 0, b = 0, count = 0;
for (let i = 0; i < imageData.length; i += 4) {
const red = imageData[i];
const green = imageData[i + 1];
const blue = imageData[i + 2];
const brightness = (red + green + blue) / 3;
// Only count light pixels (tweak threshold as needed)
if (brightness > 180) {
r += red;
g += green;
b += blue;
count++;
}
}
if (count === 0) {
resolve('#ffffff'); // fallback if no bright pixels found
} else {
r = Math.round(r / count);
g = Math.round(g / count);
b = Math.round(b / count);
resolve(`rgb(${r}, ${g}, ${b})`);
}
};
img.onerror = (e) => {
console.error('❌ Failed to load image:', e);
reject(new Error('Image could not be loaded or is blocked by CORS.'));
};
});
};
// Eg: Mar 2025
export function formatYearMonth(yearMonth) {
return yearMonth ? moment(yearMonth, "YYYY-MM").format("MMM YYYY") : "";
}
export const fixTailwindColors = (element) => {
const elements = element.querySelectorAll("*");
elements.forEach((el) => {
const style = window.getComputedStyle(el);
["color", "backgroundColor", "borderColor"].forEach((prop) => {
const value = style[prop];
if (value.includes("oklch")) {
el.style[prop] = "#000"; // or any safe fallback
}
});
});
};
// convert component to image
export async function captureElementAsImage(element) {
if (!element) throw new Error("No element provided");
const canvas = await html2canvas(element);
return canvas.toDataURL("image/png");
}
// Utility to convert base64 data URL to a File object
export const dataURLtoFile = (dataUrl, fileName) => {
const arr = dataUrl.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], fileName, { type: mime });
};

View File

@ -0,0 +1,22 @@
import { API_PATHS } from './apiPaths';
import axiosInstance from './axiosInstance';
const uploadImage = async (imageFile) => {
const formData = new FormData();
// Append image file to form data
formData.append('image', imageFile);
try {
const response = await axiosInstance.post(API_PATHS.IMAGE.UPLOAD_IMAGE, formData, {
headers: {
'Content-Type': 'multipart/form-data', // Set header for file upload
},
});
return response.data; // Return response data
} catch (error) {
console.error('Error uploading the image:', error);
throw error; // Rethrow error for handling
}
};
export default uploadImage;

View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})