diff --git a/src/components/CVPreview/CVEducation.jsx b/src/components/CVPreview/CVEducation.jsx
new file mode 100644
index 0000000..4a10055
--- /dev/null
+++ b/src/components/CVPreview/CVEducation.jsx
@@ -0,0 +1,168 @@
+import { useState } from 'react';
+import { FaEdit, FaSave, FaTimes, FaPlus, FaMinus } from 'react-icons/fa';
+import styles from './CVEducation.module.css';
+
+const CVEducation = ({ education = [], onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState([...education]);
+
+ const handleChange = (index, field, value) => {
+ const newData = [...editData];
+ newData[index] = {
+ ...newData[index],
+ [field]: value
+ };
+ setEditData(newData);
+ };
+
+ const addEducation = () => {
+ setEditData([
+ ...editData,
+ {
+ program: '',
+ institution: '',
+ startDate: '',
+ endDate: '',
+ highlights: ''
+ }
+ ]);
+ };
+
+ const removeEducation = (index) => {
+ const newData = [...editData];
+ newData.splice(index, 1);
+ setEditData(newData);
+ };
+
+ const handleSave = () => {
+ onSave(editData);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData([...education]);
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
Education
+
+
+
+
+
+
+
+ {editData.map((edu, index) => (
+
+
+
+ handleChange(index, 'program', e.target.value)}
+ />
+
+
+
+ handleChange(index, 'institution', e.target.value)}
+ />
+
+
+
+ handleChange(index, 'startDate', e.target.value)}
+ />
+
+
+
+ handleChange(index, 'endDate', e.target.value)}
+ />
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Education
+
+
+
+
+ {education.map((edu, index) => (
+
+
+ {edu.program || 'Program Not Specified'}
+
+
+ {edu.institution || 'Institution Not Specified'}
+
+
+ {edu.startDate && (
+
+ {edu.startDate}
+
+ )}
+ {edu.endDate && (
+ <>
+ -
+
+ {edu.endDate}
+
+ >
+ )}
+
+ {edu.highlights && (
+
+ {edu.highlights.split('\n').map((paragraph, pIndex) => (
+
+ {paragraph}
+
+ ))}
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+export default CVEducation;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVEducation.module.css b/src/components/CVPreview/CVEducation.module.css
new file mode 100644
index 0000000..e467266
--- /dev/null
+++ b/src/components/CVPreview/CVEducation.module.css
@@ -0,0 +1,97 @@
+.education-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+}
+
+.education-item {
+ padding: 1rem;
+ background-color: var(--color-white);
+ border-radius: 0.25rem;
+ box-shadow: 0 0.063rem 0.188rem rgb(var(--color-black) / 10%);
+ page-break-inside: avoid;
+ font-family: var(--font-secondary);
+}
+
+.education-duration {
+ color: var(--color-slategrey);
+ font-size: var(--font-size-small);
+ margin-bottom: 0.75rem;
+ font-style: italic;
+ font-family: var(--font-secondary);
+}
+
+.education-date-separator {
+ margin: 0 0.25rem;
+}
+
+.education-description {
+ margin-top: 0.75rem;
+ color: var(--color-dark);
+ font-size: var(--font-size-medium);
+ line-height: 1.5;
+ font-family: var(--font-secondary);
+}
+
+.education-paragraph {
+ margin-bottom: 0.5rem;
+ text-align: justify;
+ font-family: var(--font-secondary);
+}
+
+.degree-info {
+ color: var(--color-dark);
+ font-family: var(--font-secondary);
+ font-size: var(--font-size-medium);
+ font-weight: var(--font-weight-semi-bold);
+}
+
+.institution-info {
+ color: var(--color-dark);
+ font-family: var(--font-secondary);
+}
+
+.education-highlights {
+ margin-top: 0.5rem;
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.edit-item {
+ background-color: var(--color-white);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+}
+
+@media (width <=37.5rem) {
+ .degree-info {
+ font-size: var(--font-size-medium);
+ }
+
+ .education-item {
+ padding: 0.75rem;
+ } }
\ No newline at end of file
diff --git a/src/components/CVPreview/CVExperience.jsx b/src/components/CVPreview/CVExperience.jsx
new file mode 100644
index 0000000..33d7fe9
--- /dev/null
+++ b/src/components/CVPreview/CVExperience.jsx
@@ -0,0 +1,225 @@
+import { useState } from 'react';
+import { FaEdit, FaSave, FaTimes, FaPlus, FaMinus } from 'react-icons/fa';
+import styles from './CVExperience.module.css';
+
+const CVExperience = ({ experience = [], onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState([...experience]);
+
+ const handleChange = (index, field, value) => {
+ const newData = [...editData];
+ newData[index] = {
+ ...newData[index],
+ [field]: value
+ };
+ setEditData(newData);
+ };
+
+ const handleBulletPointChange = (expIndex, bulletIndex, value) => {
+ const newData = [...editData];
+ const newBulletPoints = [...newData[expIndex].bulletPoints];
+ newBulletPoints[bulletIndex] = value;
+ newData[expIndex] = {
+ ...newData[expIndex],
+ bulletPoints: newBulletPoints
+ };
+ setEditData(newData);
+ };
+
+ const addExperience = () => {
+ setEditData([
+ ...editData,
+ {
+ companyName: '',
+ jobTitle: '',
+ startDate: '',
+ endDate: '',
+ bulletPoints: ['']
+ }
+ ]);
+ };
+
+ const addBulletPoint = (expIndex) => {
+ const newData = [...editData];
+ newData[expIndex] = {
+ ...newData[expIndex],
+ bulletPoints: [...newData[expIndex].bulletPoints, '']
+ };
+ setEditData(newData);
+ };
+
+ const removeExperience = (index) => {
+ const newData = [...editData];
+ newData.splice(index, 1);
+ setEditData(newData);
+ };
+
+ const removeBulletPoint = (expIndex, bulletIndex) => {
+ const newData = [...editData];
+ const newBulletPoints = [...newData[expIndex].bulletPoints];
+ newBulletPoints.splice(bulletIndex, 1);
+ newData[expIndex] = {
+ ...newData[expIndex],
+ bulletPoints: newBulletPoints
+ };
+ setEditData(newData);
+ };
+
+ const handleSave = () => {
+ onSave(editData);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData([...experience]);
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
Professional Experience
+
+
+
+
+
+
+
+ {editData.map((exp, expIndex) => (
+
+
+
+ handleChange(expIndex, 'companyName', e.target.value)}
+ />
+
+
+
+ handleChange(expIndex, 'jobTitle', e.target.value)}
+ />
+
+
+
+ handleChange(expIndex, 'startDate', e.target.value)}
+ />
+
+
+
+ handleChange(expIndex, 'endDate', e.target.value)}
+ />
+
+
+
+ {exp.bulletPoints?.map((point, bulletIndex) => (
+
+
+ ))}
+
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Professional Experience
+
+
+
+ {experience && experience.length > 0 ? (
+ experience.map((exp, index) => (
+
+
+
+
{exp.jobTitle}
+
+
+
+ {exp.companyName}
+
+
+
+ {(exp.startDate || exp.endDate) && (
+
+ {exp.startDate} - {exp.endDate}
+
+ )}
+
+
+
+
+ {exp.bulletPoints && exp.bulletPoints.length > 0 ? (
+
+ {exp.bulletPoints.map((point, i) => (
+ -
+ {point}
+
+ ))}
+
+ ) : exp.description ? (
+
+ {exp.description.split('\n').map((paragraph, pIndex) => (
+
+ {paragraph}
+
+ ))}
+
+ ) : null}
+
+ ))
+ ) : (
+
No experience data available
+ )}
+
+ );
+};
+
+export default CVExperience;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVExperience.module.css b/src/components/CVPreview/CVExperience.module.css
new file mode 100644
index 0000000..9acdf54
--- /dev/null
+++ b/src/components/CVPreview/CVExperience.module.css
@@ -0,0 +1,121 @@
+.experience-item {
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background-color: var(--color-white);
+ border-radius: 0.25rem;
+ box-shadow: 0 0.063rem 0.188rem rgb(var(--color-black) / 10%);
+}
+
+.experience-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 0.5rem;
+}
+
+.job-title {
+ font-size: var(--font-size-medium);
+ font-weight: var(--font-weight-semi-bold);
+ color: var(--color-dark);
+ margin-bottom: 0.25rem;
+ font-family: var(--font-secondary);
+}
+
+.company-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.company-name {
+ font-weight: var(--font-weight-medium);
+ color: var(--color-azulblue);
+ font-family: var(--font-secondary);
+ margin-right: 1.25rem;
+}
+
+.job-duration {
+ color: var(--color-azulblue);
+ font-size: var(--font-size-small);
+ font-family: var(--font-secondary);
+}
+
+.responsibilities-list {
+ padding-left: 1.25rem;
+ margin-top: 0.5rem;
+}
+
+.responsibility-item {
+ position: relative;
+ padding-left: 0.5rem;
+ margin-bottom: 0.5rem;
+ line-height: 1.5;
+ color: var(--color-dark);
+ font-size: var(--font-size-medium);
+ font-family: var(--font-secondary);
+}
+
+.responsibility-item::before {
+ content: "•";
+ position: absolute;
+ left: -0.75rem;
+ color: var(--color-azulblue);
+ font-weight: var(--font-weight-bold);
+}
+
+.job-description {
+ margin-top: 0.5rem;
+ color: var(--color-dark);
+ font-size: var(--font-size-medium);
+ line-height: 1.5;
+ font-family: var(--font-secondary);
+}
+
+.job-paragraph {
+ margin-bottom: 0.5rem;
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.edit-item {
+ background-color: var(--color-white);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+}
+
+.bullet-point-group {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+@media (width <=37.5rem) {
+ .experience-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.25rem;
+ } }
\ No newline at end of file
diff --git a/src/components/CVPreview/CVHeader.jsx b/src/components/CVPreview/CVHeader.jsx
new file mode 100644
index 0000000..1cce377
--- /dev/null
+++ b/src/components/CVPreview/CVHeader.jsx
@@ -0,0 +1,198 @@
+import {
+ FaPhone,
+ FaEnvelope,
+ FaLinkedin,
+ FaGithub,
+ FaExternalLinkAlt,
+ FaEdit,
+ FaSave,
+ FaTimes
+} from 'react-icons/fa';
+import styles from './CVHeader.module.css';
+import { useState } from 'react';
+
+const CVHeader = ({ fullName, contact, onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState({
+ fullName: fullName || '',
+ contact: { ...contact }
+ });
+
+ const handleInputChange = (field, value) => {
+ setEditData(prev => ({
+ ...prev,
+ [field]: value
+ }));
+ };
+
+ const handleContactChange = (field, value) => {
+ setEditData(prev => ({
+ ...prev,
+ contact: {
+ ...prev.contact,
+ [field]: value
+ }
+ }));
+ };
+
+ const handleSave = () => {
+ onSave({
+ fullName: editData.fullName,
+ contact: editData.contact
+ });
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData({
+ fullName: fullName || '',
+ contact: { ...contact }
+ });
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
+
+ handleInputChange('fullName', e.target.value)}
+ />
+
+
+
+
Contact Information
+ {['email', 'phone', 'linkedin', 'github', 'portfolio'].map(
+ (field) => (
+
+
+ handleContactChange(field, e.target.value)}
+ />
+
+ )
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {fullName || 'Professional Profile'}
+
+
+
+
+
+ {contact.email && (
+
+
+ {' '}
+
+ {contact.email}
+
+ )}
+
+ {contact.phone && (
+
+
+ {' '}
+
+ +34 {contact.phone}
+
+ )}
+
+
+
+ {contact.linkedin && (
+
+
+ {' '}
+
+
+ LinkedIn
+
+
+ )}
+
+ {contact.github && (
+
+
+ {' '}
+
+
+ GitHub
+
+
+ )}
+
+ {contact.portfolio && (
+
+
+ {' '}
+
+
+ Portfolio
+
+
+ )}
+
+
+
+ );
+};
+
+export default CVHeader;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVHeader.module.css b/src/components/CVPreview/CVHeader.module.css
new file mode 100644
index 0000000..7f39294
--- /dev/null
+++ b/src/components/CVPreview/CVHeader.module.css
@@ -0,0 +1,125 @@
+.user-section {
+ display: grid;
+ grid-template-rows: 0.2fr 1fr;
+ justify-items: center;
+ background-color: var(--color-paleskyblue);
+ margin-bottom: 2rem;
+ padding: 1rem;
+}
+
+.header-edit-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+}
+
+.cv-username {
+ margin-bottom: 0.5rem;
+ font-family: var(--font-secondary);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-white);
+ font-size: 2rem;
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.personal-details {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+}
+
+.email-and-phone-box {
+ display: flex;
+ justify-content: center;
+ gap: 1.5rem;
+}
+
+.github-portfolio-linkedin-box {
+ display: flex;
+ justify-content: center;
+ gap: 1.5rem;
+}
+
+.contact-info {
+ margin-bottom: 0.3rem;
+ color: var(--color-white);
+ font-family: var(--font-secondary);
+ display: flex;
+ align-items: center;
+}
+
+.contact-label {
+ font-weight: var(--font-weight-semi-bold);
+ color: var(--color-white);
+ font-family: var(--font-secondary);
+ margin-right: 0.5rem;
+}
+
+.contact-link {
+ color: var(--color-white);
+ text-decoration: none;
+ font-family: var(--font-secondary);
+ transition: text-decoration 0.3s ease;
+}
+
+.contact-link:hover {
+ text-decoration: underline;
+}
+
+.icon-color {
+ color: var(--color-white);
+ margin-right: 0.5rem;
+}
+
+.edit-form {
+ width: 100%;
+ background-color: var(--color-white);
+ padding: 1rem;
+ border-radius: 0.5rem;
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.contact-section {
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background-color: var(--color-lightgrey);
+ border-radius: 0.5rem;
+}
+
+@media (width <=37.5rem) {
+
+ .email-and-phone-box,
+ .github-portfolio-linkedin-box {
+ flex-direction: column;
+ gap: 0.5rem;
+ align-items: center;
+ }
+
+ .cv-username {
+ font-size: 1.5rem;
+ } }
\ No newline at end of file
diff --git a/src/components/CVPreview/CVPreview.jsx b/src/components/CVPreview/CVPreview.jsx
index 3110149..912b269 100644
--- a/src/components/CVPreview/CVPreview.jsx
+++ b/src/components/CVPreview/CVPreview.jsx
@@ -1,1062 +1,184 @@
import React, { useState, useEffect } from 'react';
-import {
- FaPhone,
- FaEnvelope,
- FaLinkedin,
- FaGithub,
- FaExternalLinkAlt,
-} from 'react-icons/fa';
import styles from './CVPreview.module.css';
-
-const CVPreview = React.forwardRef(
- ({ cvData, onSave, personalInfo, onEditModeChange }, ref) => {
- const [isEditing, setIsEditing] = useState(false);
- const [editedData, setEditedData] = useState({});
-
- const parseCvData = (data) => {
- if (!data) return {};
-
- let parsedData = data;
-
- if (typeof data === 'string') {
- try {
- parsedData = JSON.parse(data);
- } catch (error) {
- console.error('Error parsing cvData:', error);
- return {};
- }
- }
-
- let experienceArray = [];
- if (Array.isArray(parsedData.experience)) {
- experienceArray = parsedData.experience.map((exp) => {
- if (typeof exp === 'string') {
- return { bulletPoints: [exp] };
- } else if (Array.isArray(exp.description)) {
- return { bulletPoints: exp.description };
- }
- return exp;
- });
- } else if (parsedData.transferableExperience) {
- if (Array.isArray(parsedData.transferableExperience)) {
- experienceArray = parsedData.transferableExperience.map((exp) => ({
- companyName: exp.company,
- jobTitle: exp.position,
- startDate: exp.startDate,
- endDate: exp.endDate,
- bulletPoints: exp.achievements || [],
- }));
- } else {
- experienceArray = [
- {
- companyName: parsedData.transferableExperience.company,
- jobTitle: parsedData.transferableExperience.position,
- startDate: parsedData.transferableExperience.startDate,
- endDate: parsedData.transferableExperience.endDate,
- bulletPoints:
- parsedData.transferableExperience.achievements || [],
- },
- ];
- }
- }
-
- let skillsData = [];
- if (Array.isArray(parsedData.skills)) {
- skillsData = parsedData.skills;
- } else if (parsedData.skills && typeof parsedData.skills === 'object') {
- skillsData = [
- ...(parsedData.skills.technical || []),
- ...(parsedData.skills.soft || []),
- ];
+import CVHeader from './CVHeader';
+import CVSummary from './CVSummary';
+import CVSkills from './CVSkills';
+import CVExperience from './CVExperience';
+import CVProjects from './CVProjects';
+import CVEducation from './CVEducation';
+
+const CVPreview = React.forwardRef(({ cvData, onSave, personalInfo }, ref) => {
+ const [cvState, setCvState] = useState({});
+
+ const parseCvData = (data) => {
+ if (!data) return {};
+
+ let parsedData = data;
+
+ if (typeof data === 'string') {
+ try {
+ parsedData = JSON.parse(data);
+ } catch (error) {
+ console.error('Error parsing cvData:', error);
+ return {};
}
-
- return {
- fullName:
- personalInfo.fullName ||
- personalInfo.name ||
- personalInfo.personalInfo?.fullName ||
- '',
- contact: {
- email:
- personalInfo.contact?.email ||
- personalInfo.email ||
- personalInfo.personalInfo?.email ||
- '',
- phone:
- personalInfo.contact?.phone ||
- personalInfo.phone ||
- personalInfo.personalInfo?.phone ||
- '',
- linkedin:
- personalInfo.contact?.linkedin ||
- personalInfo.linkedin ||
- personalInfo.personalInfo?.linkedin ||
- '',
- github:
- personalInfo.contact?.github ||
- personalInfo.github ||
- personalInfo.personalInfo?.github ||
- '',
- portfolio:
- personalInfo.contact?.portfolio ||
- personalInfo.portfolio ||
- personalInfo.personalInfo?.portfolio ||
- '',
- },
- professional_summary:
- parsedData.professionalSummary ||
- parsedData.professional_summary ||
- '',
- experience: experienceArray,
- projects: Array.isArray(parsedData.projects) ? parsedData.projects : [],
- education: Array.isArray(parsedData.education)
- ? parsedData.education
- : [],
- skills: skillsData,
- };
- };
-
- useEffect(() => {
- const parsedData = parseCvData(cvData);
- setEditedData(parsedData);
- }, [cvData]);
-
- if (!cvData)
- return No CV data available
;
-
- const parsedCvData = parseCvData(cvData);
-
- if (!parsedCvData || Object.keys(parsedCvData).length === 0) {
- return (
- Invalid or empty CV data format
- );
}
- const handleInputChange = (field, value) => {
- setEditedData((prev) => ({
- ...prev,
- [field]: value,
- }));
- };
-
- const handleContactChange = (field, value) => {
- setEditedData((prev) => ({
- ...prev,
- contact: {
- ...prev.contact,
- [field]: value,
- },
- }));
- };
-
- const handleArrayChange = (arrayName, index, field, value) => {
- setEditedData((prev) => ({
- ...prev,
- [arrayName]:
- prev[arrayName]?.map((item, i) =>
- i === index ? { ...item, [field]: value } : item
- ) || [],
- }));
- };
-
- const handleBulletPointChange = (expIndex, bulletIndex, value) => {
- setEditedData((prev) => ({
- ...prev,
- experience:
- prev.experience?.map((exp, i) =>
- i === expIndex
- ? {
- ...exp,
- bulletPoints:
- exp.bulletPoints?.map((point, j) =>
- j === bulletIndex ? value : point
- ) || [],
- }
- : exp
- ) || [],
- }));
- };
- const handleExperienceChange = (expIndex, field, value) => {
- setEditedData((prev) => {
- const newExperience = [...prev.experience];
- newExperience[expIndex] = {
- ...newExperience[expIndex],
- [field]: value,
- };
- return {
- ...prev,
- experience: newExperience,
- };
+ let experienceArray = [];
+ if (Array.isArray(parsedData.experience)) {
+ experienceArray = parsedData.experience.map((exp) => {
+ if (typeof exp === 'string') {
+ return { bulletPoints: [exp] };
+ } else if (Array.isArray(exp.description)) {
+ return { bulletPoints: exp.description };
+ }
+ return exp;
});
- };
-
- const addExperience = () => {
- setEditedData((prev) => ({
- ...prev,
- experience: [
- ...(prev.experience || []),
+ } else if (parsedData.transferableExperience) {
+ if (Array.isArray(parsedData.transferableExperience)) {
+ experienceArray = parsedData.transferableExperience.map((exp) => ({
+ companyName: exp.company,
+ jobTitle: exp.position,
+ startDate: exp.startDate,
+ endDate: exp.endDate,
+ bulletPoints: exp.achievements || [],
+ }));
+ } else {
+ experienceArray = [
{
- companyName: '',
- jobTitle: '',
- startDate: '',
- endDate: '',
- bulletPoints: [''],
+ companyName: parsedData.transferableExperience.company,
+ jobTitle: parsedData.transferableExperience.position,
+ startDate: parsedData.transferableExperience.startDate,
+ endDate: parsedData.transferableExperience.endDate,
+ bulletPoints: parsedData.transferableExperience.achievements || [],
},
- ],
- }));
- };
-
- const addProject = () => {
- setEditedData((prev) => ({
- ...prev,
- projects: [
- ...(prev.projects || []),
- {
- name: '',
- description: '',
- deployedWebsite: '',
- githubLink: '',
- technologiesUsed: [],
- },
- ],
- }));
- };
-
- const addEducation = () => {
- setEditedData((prev) => ({
- ...prev,
- education: [
- ...(prev.education || []),
- {
- program: '',
- institution: '',
- duration: '',
- highlights: '',
- },
- ],
- }));
- };
-
- const removeItem = (arrayName, index) => {
- setEditedData((prev) => ({
- ...prev,
- [arrayName]: prev[arrayName]?.filter((_, i) => i !== index) || [],
- }));
- };
-
- const addBulletPoint = (expIndex) => {
- setEditedData((prev) => ({
- ...prev,
- experience:
- prev.experience?.map((exp, i) =>
- i === expIndex
- ? {
- ...exp,
- bulletPoints: [...(exp.bulletPoints || []), ''],
- }
- : exp
- ) || [],
- }));
- };
-
- const removeBulletPoint = (expIndex, bulletIndex) => {
- setEditedData((prev) => ({
- ...prev,
- experience:
- prev.experience?.map((exp, i) =>
- i === expIndex
- ? {
- ...exp,
- bulletPoints:
- exp.bulletPoints?.filter((_, j) => j !== bulletIndex) || [],
- }
- : exp
- ) || [],
- }));
- };
+ ];
+ }
+ }
- const handleSave = () => {
- setIsEditing(false);
- if (onSave) onSave(editedData);
- if (onEditModeChange) onEditModeChange(false);
- };
+ let skillsData = [];
+ if (Array.isArray(parsedData.skills)) {
+ skillsData = parsedData.skills;
+ } else if (parsedData.skills && typeof parsedData.skills === 'object') {
+ skillsData = [
+ ...(parsedData.skills.technical || []),
+ ...(parsedData.skills.soft || []),
+ ];
+ }
- const handleCancel = () => {
- const parsedData = parseCvData(cvData);
- setEditedData({
- fullName: parsedData.fullName || '',
- contact: parsedData.contact || {},
- professional_summary:
- parsedData.professional_summary ||
- parsedData.professionalSummary ||
+ return {
+ fullName:
+ personalInfo.fullName ||
+ personalInfo.name ||
+ personalInfo.personalInfo?.fullName ||
+ '',
+ contact: {
+ email:
+ personalInfo.contact?.email ||
+ personalInfo.email ||
+ personalInfo.personalInfo?.email ||
'',
- experience: parsedData.experience || [],
- projects: parsedData.projects || [],
- education: parsedData.education || [],
- skills: parsedData.skills || [],
- });
- setIsEditing(false);
- if (onEditModeChange) onEditModeChange(false);
- };
-
- const handleEditClick = () => {
- setIsEditing(true);
- if (onEditModeChange) onEditModeChange(true);
- };
-
- const displayData = isEditing ? editedData : parsedCvData;
-
- const normalizedData = {
- fullName: displayData.fullName || '',
- contact: displayData.contact || {},
+ phone:
+ personalInfo.contact?.phone ||
+ personalInfo.phone ||
+ personalInfo.personalInfo?.phone ||
+ '',
+ linkedin:
+ personalInfo.contact?.linkedin ||
+ personalInfo.linkedin ||
+ personalInfo.personalInfo?.linkedin ||
+ '',
+ github:
+ personalInfo.contact?.github ||
+ personalInfo.github ||
+ personalInfo.personalInfo?.github ||
+ '',
+ portfolio:
+ personalInfo.contact?.portfolio ||
+ personalInfo.portfolio ||
+ personalInfo.personalInfo?.portfolio ||
+ '',
+ },
professional_summary:
- displayData.professional_summary ||
- displayData.professionalSummary ||
+ parsedData.professionalSummary ||
+ parsedData.professional_summary ||
'',
- experience: Array.isArray(displayData.experience)
- ? displayData.experience
- : [],
- projects: Array.isArray(displayData.projects) ? displayData.projects : [],
- education: Array.isArray(displayData.education)
- ? displayData.education
- : [],
- skills: Array.isArray(displayData.skills) ? displayData.skills : [],
+ experience: experienceArray,
+ projects: Array.isArray(parsedData.projects) ? parsedData.projects : [],
+ education: Array.isArray(parsedData.education) ? parsedData.education : [],
+ skills: skillsData,
};
+ };
- const {
- fullName,
- contact,
- professional_summary,
- experience,
- projects,
- education,
- skills,
- } = normalizedData;
+ useEffect(() => {
+ const parsedData = parseCvData(cvData);
+ setCvState(parsedData);
+ }, [cvData]);
- const isSoftSkill = (skill) => {
- const skillStr = typeof skill === 'string' ? skill.toLowerCase() : '';
- const softSkills = [
- 'communication',
- 'teamwork',
- 'leadership',
- 'problem-solving',
- 'adaptability',
- 'creativity',
- 'time management',
- 'collaboration',
- 'interpersonal',
- 'negotiation',
- 'critical thinking',
- 'emotional intelligence',
- 'team player',
- 'active listening',
- 'conflict resolution',
- 'presentation',
- 'mentoring',
- 'coaching',
- 'decision making',
- 'strategic thinking',
- ];
- return softSkills.some((softSkill) =>
- skillStr.includes(softSkill.toLowerCase())
- );
- };
+ if (!cvData) return No CV data available
;
- if (isEditing) {
- return (
-
-
-
-
-
+ const parsedCvData = parseCvData(cvData);
-
-
- handleInputChange('fullName', e.target.value)}
- className={styles.input}
- />
-
-
-
-
Contact Information
- {['email', 'phone', 'linkedin', 'github', 'portfolio'].map(
- (field) => (
-
-
- handleContactChange(field, e.target.value)}
- className={styles.input}
- />
-
- )
- )}
-
-
-
-
-
-
-
-
Experience
- {experience.map((exp, expIndex) => (
-
-
-
-
- handleExperienceChange(
- expIndex,
- 'companyName',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleExperienceChange(
- expIndex,
- 'jobTitle',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleExperienceChange(
- expIndex,
- 'startDate',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleExperienceChange(
- expIndex,
- 'endDate',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
- {exp.bulletPoints?.map((point, bulletIndex) => (
-
-
- ))}
-
-
-
-
- ))}
-
-
-
-
-
-
+ if (!parsedCvData || Object.keys(parsedCvData).length === 0) {
+ return
Invalid or empty CV data format
;
+ }
-
-
Projects
- {projects.map((project, index) => (
-
-
-
-
- handleArrayChange(
- 'projects',
- index,
- 'name',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
-
-
-
- handleArrayChange(
- 'projects',
- index,
- 'deployedLink',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleArrayChange(
- 'projects',
- index,
- 'githubLink',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleArrayChange(
- 'projects',
- index,
- 'technologiesUsed',
- e.target.value.split(',').map((tech) => tech.trim())
- )
- }
- className={styles.input}
- />
-
-
-
- ))}
-
-
+ const handleSave = (section, data) => {
+ setCvState(prev => ({
+ ...prev,
+ [section]: data
+ }));
+
+ if (onSave) {
+ onSave({
+ ...cvState,
+ [section]: data
+ });
+ }
+ };
-
-
Education
- {education.map((edu, index) => (
-
-
-
-
- handleArrayChange(
- 'education',
- index,
- 'program',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleArrayChange(
- 'education',
- index,
- 'institution',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleArrayChange(
- 'education',
- index,
- 'startDate',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
- handleArrayChange(
- 'education',
- index,
- 'endDate',
- e.target.value
- )
- }
- className={styles.input}
- />
-
-
-
-
-
-
- ))}
-
-
-
- );
- }
+ const { fullName, contact, professional_summary, experience, projects, education, skills } = cvState;
return (
-
-
-
- {fullName || 'Professional Profile'}
-
-
-
- {contact.email && (
-
-
- {' '}
-
- {contact.email}
-
- )}
-
- {contact.phone && (
-
-
- {' '}
- {' '}
-
- +34 {contact.phone}
-
- )}
-
-
-
- {contact.linkedin && (
-
-
- {' '}
-
-
- LinkedIn
-
-
- )}
-
- {contact.github && (
-
-
- {' '}
- {' '}
-
-
- GitHub
-
-
- )}
-
- {contact.portfolio && (
-
-
- {' '}
- {' '}
-
-
- Portfolio
-
-
- )}
-
-
-
-
-
-
Professional Summary
-
-
- {professional_summary.split('\n').map((paragraph, index) => (
-
- {paragraph}
-
- ))}
-
-
-
+
handleSave('header', data)}
+ />
+
+ handleSave('professional_summary', data)}
+ />
+
{skills && skills.length > 0 && (
-
-
Skills
-
-
-
-
Technical Skills
-
- {skills
- .filter((skill) => !isSoftSkill(skill))
- .map((skill, index) => (
-
- {skill}
-
- ))}
-
-
-
-
Soft Skills
-
- {skills.filter(isSoftSkill).map((skill, index) => (
-
- {skill}
-
- ))}
-
-
-
-
+ handleSave('skills', data)}
+ />
)}
-
-
-
Professional Experience
-
- {experience && experience.length > 0 ? (
- experience.map((exp, index) => (
-
-
-
-
{exp.jobTitle}
-
-
-
- {exp.companyName}
-
-
-
- {(exp.startDate || exp.endDate) && (
-
- {exp.startDate} - {exp.endDate}
-
- )}
-
-
-
-
- {exp.bulletPoints && exp.bulletPoints.length > 0 ? (
-
- {exp.bulletPoints.map((point, i) => (
- -
- {point}
-
- ))}
-
- ) : exp.description ? (
-
- {exp.description.split('\n').map((paragraph, pIndex) => (
-
- {paragraph}
-
- ))}
-
- ) : null}
-
- ))
- ) : (
-
No experience data available
- )}
-
-
+
+ handleSave('experience', data)}
+ />
+
{projects && projects.length > 0 && (
-
-
Projects
-
-
- {projects.map((project, index) => (
-
-
- {project.name || 'Project Name Not Specified'}
-
- {project.description && (
-
- {project.description
- .split('\n')
- .map((paragraph, pIndex) => (
-
- {paragraph}
-
- ))}
-
- )}
-
- {project.technologiesUsed &&
- project.technologiesUsed.length > 0 && (
-
-
- Technologies:{' '}
-
-
- {project.technologiesUsed.map((tech, techIndex) => (
-
- {tech}
-
- ))}
-
-
- )}
-
-
- ))}
-
-
+ handleSave('projects', data)}
+ />
)}
-
+
{education && education.length > 0 && (
-
-
Education
-
-
- {education.map((edu, index) => (
-
-
- {edu.program || 'Program Not Specified'}
-
-
- {edu.institution || 'Institution Not Specified'}
-
-
- {edu.startDate && (
-
- {edu.startDate}
-
- )}
- {edu.endDate && (
- <>
-
- {' '}
- -{' '}
-
-
- {edu.endDate}
-
- >
- )}
-
- {edu.highlights && (
-
- {edu.highlights.split('\n').map((paragraph, pIndex) => (
-
- {paragraph}
-
- ))}
-
- )}
-
- ))}
-
-
+ handleSave('education', data)}
+ />
)}
-
-
-
-
);
});
-export default CVPreview;
+export default CVPreview;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVPreview.module.css b/src/components/CVPreview/CVPreview.module.css
index e21380b..0b8726a 100644
--- a/src/components/CVPreview/CVPreview.module.css
+++ b/src/components/CVPreview/CVPreview.module.css
@@ -12,6 +12,7 @@
}
@media print {
+
.button-container,
.no-print {
display: none !important;
@@ -64,11 +65,9 @@
.update-button {
padding: 0.75rem 1.5rem;
- background: linear-gradient(
- 135deg,
- var(--color-paleskyblue),
- var(--color-slategrey) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-paleskyblue),
+ var(--color-slategrey) 100%);
color: var(--color-white);
border: none;
cursor: pointer;
@@ -87,11 +86,9 @@
.update-button:hover {
transform: translateY(-0.125rem);
box-shadow: 0 0.5rem 1.57rem rgb(var(--color-slategrey) / 60%);
- background: linear-gradient(
- 135deg,
- var(--color-slategrey) 0%,
- var(--color-paleskyblue) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-slategrey) 0%,
+ var(--color-paleskyblue) 100%);
}
.update-button:active {
@@ -106,12 +103,10 @@
left: -100%;
width: 100%;
height: 100%;
- background: linear-gradient(
- 90deg,
- transparent,
- rgb(255 255 255 / 20%),
- transparent
- );
+ background: linear-gradient(90deg,
+ transparent,
+ rgb(255 255 255 / 20%),
+ transparent);
transition: left 0.5s;
}
@@ -122,11 +117,9 @@
.save-button {
margin-right: 0.625rem;
padding: 0.625rem 1.25rem;
- background: linear-gradient(
- 135deg,
- var(--color-success) 0%,
- var(--color-teal) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-success) 0%,
+ var(--color-teal) 100%);
color: var(--color-white);
border: none;
cursor: pointer;
@@ -140,20 +133,16 @@
.save-button:hover {
transform: translateY(-0.063rem);
box-shadow: 0 0.25rem 0.938rem var(--color-success);
- background: linear-gradient(
- 135deg,
- var(--color-teal) 0%,
- var(--color-success) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-teal) 0%,
+ var(--color-success) 100%);
}
.cancel-button {
padding: 0.625rem 1.25rem;
- background: linear-gradient(
- 135deg,
- var(--color-slategrey) 0%,
- var(--color-dark) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-slategrey) 0%,
+ var(--color-dark) 100%);
color: var(--color-white);
border: none;
cursor: pointer;
@@ -167,11 +156,9 @@
.cancel-button:hover {
transform: translateY(-0.063rem);
box-shadow: 0 0.25rem 0.938rem var(--color-slategrey);
- background: linear-gradient(
- 135deg,
- var(--color-dark) 0%,
- var(--color-slategrey) 100%
- );
+ background: linear-gradient(135deg,
+ var(--color-dark) 0%,
+ var(--color-slategrey) 100%);
}
.add-button {
@@ -365,65 +352,6 @@
font-family: var(--font-secondary);
}
-.contact-section {
- margin-bottom: 1.5rem;
-}
-
-.user-section {
- display: grid;
- grid-template-rows: 0.2fr 1fr;
- justify-items: center;
- background-color: var(--color-paleskyblue);
- margin-bottom: 2rem;
- padding: 1rem;
-}
-
-.cv-username {
- margin-bottom: 0.5rem;
- font-family: var(--font-secondary);
- font-weight: var(--font-weight-bold);
-}
-
-.email-and-phone-box {
- display: flex;
- justify-content: center;
- gap: 1.5rem;
-}
-
-.github-portfolio-linkedin-box {
- display: flex;
- justify-content: center;
- gap: 1.5rem;
-}
-
-.contact-info {
- margin-bottom: 0.3rem;
- color: var(--color-black);
- font-family: var(--font-secondary);
-}
-
-.contact-label {
- font-weight: var(--font-weight-semi-bold);
- color: var(--color-black);
- font-family: var(--font-secondary);
-}
-
-.contact-link {
- color: var(--color-azulblue);
- text-decoration: none;
- font-family: var(--font-secondary);
- transition: text-decoration 0.3s ease;
-}
-
-.contact-link:hover {
- text-decoration: underline;
-}
-
-.icon-color {
- color: var(--color-slategrey);
- margin-right: 0.5rem;
-}
-
.line {
border: none;
height: 0.5px;
@@ -431,303 +359,20 @@
margin-bottom: 1rem;
}
-.summary-paragraph {
- position: relative;
- padding-left: 0.5rem;
- margin-bottom: 0.5rem;
- line-height: 1.5;
- color: var(--color-dark);
- font-size: var(--font-size-medium);
- font-family: var(--font-secondary);
-}
-
-.skills-container {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 6rem;
-}
-
-.skills-column {
- margin-bottom: 1.5rem;
-}
-
-.skills-subheader {
- color: var(--color-dark);
- font-size: 1.1rem;
- font-weight: var(--font-weight-semi-bold);
- margin-bottom: 0.75rem;
- padding-bottom: 0.25rem;
- border-bottom: 0.063rem solid var(--color-white);
- font-family: var(--font-secondary);
-}
-
-.skills-list {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.skill-item {
- background-color: var(--color-paleskyblue);
- border: 0.065rem solid var(--color-royalblue);
- color: var(--color-dark);
- padding: 0.4rem 0.8rem;
- border-radius: 0.25rem;
- font-size: var(--font-size-medium);
- font-family: var(--font-secondary);
- display: inline-block;
- position: relative;
- padding-left: 0.5rem;
-}
-
-.skills-column:nth-child(2) .skill-item {
- background-color: var(--color-paleskyblue);
- border: 0.065rem solid var(--color-royalblue);
- color: var(--color-dark);
-}
-
-.experience-item {
- margin-bottom: 1.5rem;
- padding: 1rem;
- background-color: var(--color-white);
- border-radius: 0.25rem;
- box-shadow: 0 0.063rem 0.188rem rgb(var(--color-black) / 10%);
-}
-
-.experience-header {
- display: flex;
- justify-content: space-between;
- align-items: baseline;
- margin-bottom: 0.5rem;
-}
-
-.job-title {
- font-size: var(--font-size-medium);
- font-weight: var(--font-weight-semi-bold);
- color: #2c3e50;
- margin-bottom: 0.25rem;
- font-family: var(--font-secondary);
-}
-
-.company-info {
- display: flex;
- flex-direction: column;
-}
-
-.company-name {
- font-weight: var(--font-weight-medium);
- color: var(--color-azulblue);
- font-family: var(--font-secondary);
- margin-right: 1.25rem;
-}
-
-.job-duration {
- color: var(--color-azulblue);
- font-size: var(--font-size-small);
- font-family: var(--font-secondary);
-}
-
-.responsibilities-list {
- padding-left: 1.25rem;
- margin-top: 0.5rem;
-}
-
-.responsibility-item {
- position: relative;
- padding-left: 0.5rem;
- margin-bottom: 0.5rem;
- line-height: 1.5;
- color: var(--color-dark);
- font-size: var(--font-size-medium);
- font-family: var(--font-secondary);
-}
-
-.responsibility-item::before {
- content: "•";
- position: absolute;
- left: -0.75rem;
- color: var(--color-azulblue);
- font-weight: var(--font-weight-bold);
-}
-
-.job-description {
- margin-top: 0.5rem;
- color: var(--color-dark);
- font-size: var(--font-size-medium);
- line-height: 1.5;
- font-family: var(--font-secondary);
-}
-
-.job-paragraph {
- margin-bottom: 0.5rem;
-}
-
-.project-item {
- margin-bottom: 1.5rem;
- padding-bottom: 1rem;
- border-bottom: 0.063rem solid var(--color-white);
-}
-
-.project-item:last-child {
- border-bottom: none;
- margin-bottom: 0;
- padding-bottom: 0;
-}
-
-.project-name {
- font-size: var(--font-size-medium);
- font-weight: var(--font-weight-semi-bold);
- color: var(--color-dark);
- margin-bottom: 0.5rem;
- font-family: var(--font-secondary);
-}
-
-.project-description {
- color: var(--color-dark);
- font-size: var(--font-size-medium);
- line-height: 1.5;
- margin-bottom: 0.75rem;
- font-family: var(--font-secondary);
-}
-
-.project-paragraph {
- margin-bottom: 0.5rem;
- font-family: var(--font-secondary);
-}
-
-.project-paragraph:last-child {
- margin-bottom: 0;
-}
-
-.project-links-container {
- display: flex;
- gap: 3rem;
- margin-bottom: 0.75rem;
- margin-top: 0.75rem;
-}
-
-.project-link {
- color: var(--color-azulblue);
- text-decoration: none;
- font-size: var(--font-size-medium);
- font-family: var(--font-secondary);
- display: inline-block;
- transition: text-decoration 0.3s ease;
-}
-
-.project-link:hover {
- text-decoration: underline;
-}
-
-.technologies-container {
- margin-top: 0.5rem;
-}
-
-.technologies-label {
- font-size: var(--font-size-medium);
- color: var(--color-dark);
- font-weight: var(--font-weight-bold);
- font-family: var(--font-secondary);
-}
-
-.technologies-list {
- display: inline-flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-left: 0.5rem;
-}
-
-.technology-tag {
- background-color: var(--color-hero2);
- color: var(--color-dark);
- padding: 0.25rem 0.5rem;
- border-radius: 0.188rem;
- font-size: var(--font-small);
- font-family: var(--font-secondary);
- white-space: nowrap;
- transition: background-color 0.3s ease;
-}
-
.no-data {
color: var(--color-black);
font-style: italic;
font-family: var(--font-secondary);
}
-.education-list {
+.section-header-container {
display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.education-item {
- padding: 1rem;
- background-color: var(--color-white);
- border-radius: 0.25rem;
- box-shadow: 0 0.063rem 0.188rem rgb(var(--color-black) / 10%);
- page-break-inside: avoid;
- font-family: var(--font-secondary);
-}
-
-.education-duration {
- color: rgb(var(--color-slategrey) / 40%);
- font-size: var(--font-size-small);
- margin-bottom: 0.75rem;
- font-style: italic;
- font-family: var(--font-secondary);
-}
-
-.education-date-separator {
- margin: 0 0.25rem;
-}
-
-.education-description {
- margin-top: 0.75rem;
- color: rgb(var(--color-black) / 10%);
- font-size: var(--font-size-medium);
- line-height: 1.5;
- font-family: var(--font-secondary);
-}
-
-.education-paragraph {
- margin-bottom: 0.5rem;
- text-align: justify;
- font-family: var(--font-secondary);
-}
-
-.degree-info {
- color: var(--color-dark);
- font-family: var(--font-secondary);
-}
-
-.institution-info {
- color: var(--color-dark);
- font-family: var(--font-secondary);
+ justify-content: space-between;
+ align-items: center;
}
-@media (width <= 37.5rem) {
+@media (width <=37.5rem) {
.cv-container {
padding: 1rem;
}
-
- .degree-info {
- font-size: var(--font-size-medium);
- }
-
- .education-item,
- .experience-item {
- padding: 0.75rem;
- }
-
- .experience-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.25rem;
- }
-
- .project-links-container {
- flex-direction: column;
- gap: 0.5rem;
- }
-}
+}
\ No newline at end of file
diff --git a/src/components/CVPreview/CVProjects.jsx b/src/components/CVPreview/CVProjects.jsx
new file mode 100644
index 0000000..e896f1b
--- /dev/null
+++ b/src/components/CVPreview/CVProjects.jsx
@@ -0,0 +1,211 @@
+import { useState } from 'react';
+import { FaGithub, FaExternalLinkAlt, FaEdit, FaSave, FaTimes, FaPlus, FaMinus } from 'react-icons/fa';
+import styles from './CVProjects.module.css';
+
+const CVProjects = ({ projects = [], onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState([...projects]);
+
+ const handleChange = (index, field, value) => {
+ const newData = [...editData];
+ newData[index] = {
+ ...newData[index],
+ [field]: value
+ };
+ setEditData(newData);
+ };
+
+ const handleTechChange = (index, value) => {
+ const newData = [...editData];
+ newData[index] = {
+ ...newData[index],
+ technologiesUsed: value.split(',').map(tech => tech.trim())
+ };
+ setEditData(newData);
+ };
+
+ const addProject = () => {
+ setEditData([
+ ...editData,
+ {
+ name: '',
+ description: '',
+ deployedLink: '',
+ githubLink: '',
+ technologiesUsed: []
+ }
+ ]);
+ };
+
+ const removeProject = (index) => {
+ const newData = [...editData];
+ newData.splice(index, 1);
+ setEditData(newData);
+ };
+
+ const handleSave = () => {
+ onSave(editData);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData([...projects]);
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
Projects
+
+
+
+
+
+
+
+ {editData.map((project, index) => (
+
+
+
+ handleChange(index, 'name', e.target.value)}
+ />
+
+
+
+
+
+
+ handleChange(index, 'deployedLink', e.target.value)}
+ />
+
+
+
+ handleChange(index, 'githubLink', e.target.value)}
+ />
+
+
+
+ handleTechChange(index, e.target.value)}
+ />
+
+
+
+ ))}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Projects
+
+
+
+
+ {projects.map((project, index) => (
+
+
+ {project.name || 'Project Name Not Specified'}
+
+ {project.description && (
+
+ {project.description
+ .split('\n')
+ .map((paragraph, pIndex) => (
+
+ {paragraph}
+
+ ))}
+
+ )}
+
+ {project.technologiesUsed && project.technologiesUsed.length > 0 && (
+
+
+ Technologies:{' '}
+
+
+ {project.technologiesUsed.map((tech, techIndex) => (
+
+ {tech}
+
+ ))}
+
+
+ )}
+
+
+ ))}
+
+
+ );
+};
+
+export default CVProjects;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVProjects.module.css b/src/components/CVPreview/CVProjects.module.css
new file mode 100644
index 0000000..5744179
--- /dev/null
+++ b/src/components/CVPreview/CVProjects.module.css
@@ -0,0 +1,125 @@
+.project-item {
+ margin-bottom: 1.5rem;
+ padding-bottom: 1rem;
+ border-bottom: 0.063rem solid var(--color-white);
+}
+
+.project-item:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+
+.project-name {
+ font-size: var(--font-size-medium);
+ font-weight: var(--font-weight-semi-bold);
+ color: var(--color-dark);
+ margin-bottom: 0.5rem;
+ font-family: var(--font-secondary);
+}
+
+.project-description {
+ color: var(--color-dark);
+ font-size: var(--font-size-medium);
+ line-height: 1.5;
+ margin-bottom: 0.75rem;
+ font-family: var(--font-secondary);
+}
+
+.project-paragraph {
+ margin-bottom: 0.5rem;
+ font-family: var(--font-secondary);
+}
+
+.project-paragraph:last-child {
+ margin-bottom: 0;
+}
+
+.project-links-container {
+ display: flex;
+ gap: 1.5rem;
+ margin-bottom: 0.75rem;
+ margin-top: 0.75rem;
+}
+
+.project-link {
+ color: var(--color-azulblue);
+ text-decoration: none;
+ font-size: var(--font-size-medium);
+ font-family: var(--font-secondary);
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: text-decoration 0.3s ease;
+}
+
+.project-link:hover {
+ text-decoration: underline;
+}
+
+.technologies-container {
+ margin-top: 0.5rem;
+}
+
+.technologies-label {
+ font-size: var(--font-size-medium);
+ color: var(--color-dark);
+ font-weight: var(--font-weight-bold);
+ font-family: var(--font-secondary);
+}
+
+.technologies-list {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-left: 0.5rem;
+}
+
+.technology-tag {
+ background-color: var(--color-hero2);
+ color: var(--color-dark);
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.188rem;
+ font-size: var(--font-small);
+ font-family: var(--font-secondary);
+ white-space: nowrap;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.edit-item {
+ background-color: var(--color-white);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+}
+
+@media (width <=37.5rem) {
+ .project-links-container {
+ flex-direction: column;
+ gap: 0.5rem;
+ } }
\ No newline at end of file
diff --git a/src/components/CVPreview/CVSkills.jsx b/src/components/CVPreview/CVSkills.jsx
new file mode 100644
index 0000000..07d55e5
--- /dev/null
+++ b/src/components/CVPreview/CVSkills.jsx
@@ -0,0 +1,124 @@
+import { useState } from 'react';
+import { FaEdit, FaSave, FaTimes } from 'react-icons/fa';
+import styles from './CVSkills.module.css';
+
+const CVSkills = ({ skills = [], onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState(skills.join(', '));
+
+ const isSoftSkill = (skill) => {
+ const skillStr = typeof skill === 'string' ? skill.toLowerCase() : '';
+ const softSkills = [
+ 'communication',
+ 'teamwork',
+ 'leadership',
+ 'problem-solving',
+ 'adaptability',
+ 'creativity',
+ 'time management',
+ 'collaboration',
+ 'interpersonal',
+ 'negotiation',
+ 'critical thinking',
+ 'emotional intelligence',
+ 'team player',
+ 'active listening',
+ 'conflict resolution',
+ 'presentation',
+ 'mentoring',
+ 'coaching',
+ 'decision making',
+ 'strategic thinking',
+ ];
+ return softSkills.some((softSkill) =>
+ skillStr.includes(softSkill.toLowerCase())
+ );
+ };
+
+ const handleSave = () => {
+ const newSkills = editData
+ .split(',')
+ .map(skill => skill.trim())
+ .filter(skill => skill);
+ onSave(newSkills);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData(skills.join(', '));
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
Skills
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
Skills
+
+
+
+
+
+
Technical Skills
+
+ {skills
+ .filter((skill) => !isSoftSkill(skill))
+ .map((skill, index) => (
+
+ {skill}
+
+ ))}
+
+
+
+
Soft Skills
+
+ {skills.filter(isSoftSkill).map((skill, index) => (
+
+ {skill}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default CVSkills;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVSkills.module.css b/src/components/CVPreview/CVSkills.module.css
new file mode 100644
index 0000000..73be620
--- /dev/null
+++ b/src/components/CVPreview/CVSkills.module.css
@@ -0,0 +1,80 @@
+.skills-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+}
+
+.skills-column {
+ margin-bottom: 1.5rem;
+}
+
+.skills-subheader {
+ color: var(--color-dark);
+ font-size: 1.1rem;
+ font-weight: var(--font-weight-semi-bold);
+ margin-bottom: 0.75rem;
+ padding-bottom: 0.25rem;
+ border-bottom: 0.063rem solid var(--color-white);
+ font-family: var(--font-secondary);
+}
+
+.skills-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.skill-item {
+ background-color: var(--color-paleskyblue);
+ border: 0.065rem solid var(--color-royalblue);
+ color: var(--color-dark);
+ padding: 0.4rem 0.8rem;
+ border-radius: 0.25rem;
+ font-size: var(--font-size-medium);
+ font-family: var(--font-secondary);
+ display: inline-block;
+}
+
+.skills-column:nth-child(2) .skill-item {
+ background-color: var(--color-paleskyblue);
+ border: 0.065rem solid var(--color-royalblue);
+ color: var(--color-dark);
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.edit-form {
+ background-color: var(--color-white);
+ padding: 1rem;
+ border-radius: 0.5rem;
+}
+
+@media (width <=37.5rem) {
+ .skills-container {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ } }
\ No newline at end of file
diff --git a/src/components/CVPreview/CVSummary.jsx b/src/components/CVPreview/CVSummary.jsx
new file mode 100644
index 0000000..1ca7c24
--- /dev/null
+++ b/src/components/CVPreview/CVSummary.jsx
@@ -0,0 +1,68 @@
+import { useState } from 'react';
+import { FaEdit, FaSave, FaTimes } from 'react-icons/fa';
+import styles from './CVSummary.module.css';
+
+const CVSummary = ({ professional_summary = '', onSave }) => {
+ const [isEditing, setIsEditing] = useState(false);
+ const [editData, setEditData] = useState(professional_summary);
+
+ const handleSave = () => {
+ onSave(editData);
+ setIsEditing(false);
+ };
+
+ const handleCancel = () => {
+ setEditData(professional_summary);
+ setIsEditing(false);
+ };
+
+ if (isEditing) {
+ return (
+
+
+
Professional Summary
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
Professional Summary
+
+
+
+
+ {professional_summary.split('\n').map((paragraph, index) => (
+
+ {paragraph}
+
+ ))}
+
+
+ );
+};
+
+export default CVSummary;
\ No newline at end of file
diff --git a/src/components/CVPreview/CVSummary.module.css b/src/components/CVPreview/CVSummary.module.css
new file mode 100644
index 0000000..be91a6c
--- /dev/null
+++ b/src/components/CVPreview/CVSummary.module.css
@@ -0,0 +1,51 @@
+.summary-content {
+ color: var(--color-dark);
+ font-size: var(--font-size-medium);
+ line-height: 1.6;
+ font-family: var(--font-secondary);
+}
+
+.summary-paragraph {
+ margin-bottom: 1rem;
+ text-align: justify;
+}
+
+.edit-button {
+ background-color: var(--color-azulblue);
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: background-color 0.3s ease;
+}
+
+.edit-button:hover {
+ background-color: var(--color-royalblue);
+}
+
+.button-group {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.edit-textarea {
+ width: 100%;
+ min-height: 10rem;
+ padding: 1rem;
+ border: 1px solid var(--color-paleskyblue);
+ border-radius: 0.5rem;
+ font-family: var(--font-secondary);
+ resize: vertical;
+}
+
+.edit-form {
+ background-color: var(--color-white);
+ padding: 1rem;
+ border-radius: 0.5rem; }
\ No newline at end of file