diff --git a/cv-engine/src/App.tsx b/cv-engine/src/App.tsx
index e626af4..0986189 100644
--- a/cv-engine/src/App.tsx
+++ b/cv-engine/src/App.tsx
@@ -1,6 +1,9 @@
import React from 'react';
import Stepper from './components/Stepper';
import PersonalEditor from './editors/PersonalEditor';
+import WorkEditor from './editors/WorkEditor';
+import EducationEditor from './editors/EducationEditor';
+import SkillsEditor from './editors/SkillsEditor';
import PreviewPanel from './components/PreviewPanel';
import { useActiveStep } from './store/cvStore';
@@ -13,11 +16,11 @@ const App: React.FC = () => {
case 'personal':
return ;
case 'work':
- return
Work Experience editor will be implemented in M2
;
+ return ;
case 'education':
- return Education editor will be implemented in M2
;
+ return ;
case 'skills':
- return Skills editor will be implemented in M2
;
+ return ;
case 'summary':
return Summary editor will be implemented in M3
;
case 'finalize':
diff --git a/cv-engine/src/editors/EducationEditor.tsx b/cv-engine/src/editors/EducationEditor.tsx
new file mode 100644
index 0000000..e3d924b
--- /dev/null
+++ b/cv-engine/src/editors/EducationEditor.tsx
@@ -0,0 +1,130 @@
+import React from 'react';
+import { useEducationData, useCvStore } from '../store/cvStore';
+
+const EducationEditor: React.FC = () => {
+ const education = useEducationData();
+ const { addEducationItem, updateEducationItem, removeEducationItem, reorderEducationItems } = useCvStore();
+
+ const handleFieldChange = (id: string, field: string, value: string) => {
+ updateEducationItem(id, { [field]: value } as any);
+ };
+
+ return (
+
+
+
Education
+
+
+
+ {education.length === 0 && (
+
No education items yet. Click "Add Education" to start.
+ )}
+
+
+ {education.map((ed, idx) => {
+ const requiredError = {
+ degree: !ed.degree,
+ school: !ed.school,
+ startDate: !ed.startDate,
+ };
+ return (
+
+
+
Education #{idx + 1}
+
+
+
+
+
+
+
+
+
+
+
handleFieldChange(ed.id, 'degree', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.degree ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.degree &&
Degree is required
}
+
+
+
+
handleFieldChange(ed.id, 'school', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.school ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.school &&
School name is required
}
+
+
+
+
+
handleFieldChange(ed.id, 'startDate', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.startDate ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.startDate &&
Start date is required
}
+
+
+
+
handleFieldChange(ed.id, 'endDate', e.target.value)}
+ className="w-full px-3 py-2 border rounded-md border-gray-300"
+ />
+
Leave empty if ongoing
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default EducationEditor;
\ No newline at end of file
diff --git a/cv-engine/src/editors/SkillsEditor.tsx b/cv-engine/src/editors/SkillsEditor.tsx
new file mode 100644
index 0000000..86f0337
--- /dev/null
+++ b/cv-engine/src/editors/SkillsEditor.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from 'react';
+import { useSkillsData, useCvStore } from '../store/cvStore';
+
+const SkillsEditor: React.FC = () => {
+ const skills = useSkillsData();
+ const { addSkill, updateSkill, removeSkill } = useCvStore();
+ const [newSkillName, setNewSkillName] = useState('');
+ const [newSkillLevel, setNewSkillLevel] = useState<'Beginner' | 'Intermediate' | 'Advanced' | undefined>(undefined);
+
+ const handleAddSkill = () => {
+ const name = newSkillName.trim();
+ if (!name) return;
+ addSkill(name, newSkillLevel);
+ setNewSkillName('');
+ setNewSkillLevel(undefined);
+ };
+
+ return (
+
+
Skills
+
+
+
+
+ setNewSkillName(e.target.value)}
+ placeholder="e.g. React, TypeScript, SQL"
+ className="w-full px-3 py-2 border rounded-md border-gray-300"
+ />
+
+
+
+
+
+
+
+
+ {skills.length === 0 && (
+
No skills added yet. Add skills to showcase your strengths.
+ )}
+
+
+ {skills.map((skill) => (
+
+ updateSkill(skill.id, e.target.value, skill.level)}
+ className="flex-1 px-3 py-2 border rounded-md border-gray-300"
+ />
+
+
+
+ ))}
+
+
+ );
+};
+
+export default SkillsEditor;
\ No newline at end of file
diff --git a/cv-engine/src/editors/WorkEditor.tsx b/cv-engine/src/editors/WorkEditor.tsx
new file mode 100644
index 0000000..415f9a4
--- /dev/null
+++ b/cv-engine/src/editors/WorkEditor.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import { useWorkData, useCvStore } from '../store/cvStore';
+
+const WorkEditor: React.FC = () => {
+ const work = useWorkData();
+ const {
+ addWorkItem,
+ updateWorkItem,
+ removeWorkItem,
+ reorderWorkItems,
+ } = useCvStore();
+
+ const handleFieldChange = (id: string, field: string, value: string) => {
+ updateWorkItem(id, { [field]: value } as any);
+ };
+
+ const handleAddBullet = (id: string) => {
+ const item = work.find(w => w.id === id);
+ if (!item) return;
+ if ((item.bullets || []).length >= 6) return;
+ const bullets = [...(item.bullets || []), ''];
+ updateWorkItem(id, { bullets });
+ };
+
+ const handleBulletChange = (id: string, index: number, value: string) => {
+ const item = work.find(w => w.id === id);
+ if (!item) return;
+ const bullets = [...(item.bullets || [])];
+ bullets[index] = value;
+ updateWorkItem(id, { bullets });
+ };
+
+ const handleRemoveBullet = (id: string, index: number) => {
+ const item = work.find(w => w.id === id);
+ if (!item) return;
+ const bullets = [...(item.bullets || [])];
+ bullets.splice(index, 1);
+ updateWorkItem(id, { bullets });
+ };
+
+ return (
+
+
+
Work Experience
+
+
+
+ {work.length === 0 && (
+
No jobs added yet. Click "Add Job" to start.
+ )}
+
+
+ {work.map((job, idx) => {
+ const requiredError = {
+ title: !job.title,
+ company: !job.company,
+ startDate: !job.startDate,
+ };
+ return (
+
+
+
Job #{idx + 1}
+
+
+
+
+
+
+
+
+
+
+
handleFieldChange(job.id, 'title', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.title ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.title &&
Job title is required
}
+
+
+
+
handleFieldChange(job.id, 'company', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.company ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.company &&
Company name is required
}
+
+
+
+ handleFieldChange(job.id, 'location', e.target.value)}
+ className="w-full px-3 py-2 border rounded-md border-gray-300"
+ />
+
+
+
+
+
handleFieldChange(job.id, 'startDate', e.target.value)}
+ className={`w-full px-3 py-2 border rounded-md ${requiredError.startDate ? 'border-red-500' : 'border-gray-300'}`}
+ />
+ {requiredError.startDate &&
Start date is required
}
+
+
+
+
handleFieldChange(job.id, 'endDate', e.target.value)}
+ className="w-full px-3 py-2 border rounded-md border-gray-300"
+ />
+
Leave empty if current
+
+
+
+
+
+
+
+
+
+
Max 6 bullets. Aim for concise, impactful achievements (≤160 chars).
+
+ {(job.bullets || []).map((b, i) => {
+ const tooLong = (b || '').length > 160;
+ return (
+
+ );
+ })}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default WorkEditor;
\ No newline at end of file
diff --git a/cv-engine/src/store/cvStore.ts b/cv-engine/src/store/cvStore.ts
index 762ed0f..28c98e8 100644
--- a/cv-engine/src/store/cvStore.ts
+++ b/cv-engine/src/store/cvStore.ts
@@ -285,8 +285,48 @@ export const useCvStore = create((set, get) => ({
}));
}
}
-
- // Add validation for other sections as needed
+ if (activeStep === 'work') {
+ const result = validateSection('work', cv.work);
+ isValid = result.success;
+ if (!result.success) {
+ set((state) => ({
+ errors: {
+ ...state.errors,
+ work: result.error.issues.map(i => i.message)
+ }
+ }));
+ } else {
+ set((state) => ({ errors: { ...state.errors, work: [] } }));
+ }
+ }
+ if (activeStep === 'education') {
+ const result = validateSection('education', cv.education);
+ isValid = result.success;
+ if (!result.success) {
+ set((state) => ({
+ errors: {
+ ...state.errors,
+ education: result.error.issues.map(i => i.message)
+ }
+ }));
+ } else {
+ set((state) => ({ errors: { ...state.errors, education: [] } }));
+ }
+ }
+ if (activeStep === 'skills') {
+ const result = validateSection('skills', cv.skills);
+ isValid = result.success;
+ if (!result.success) {
+ set((state) => ({
+ errors: {
+ ...state.errors,
+ skills: result.error.issues.map(i => i.message)
+ }
+ }));
+ } else {
+ set((state) => ({ errors: { ...state.errors, skills: [] } }));
+ }
+ }
return isValid;
},