diff --git a/cv-engine/src/api/export.ts b/cv-engine/src/api/export.ts index 4d0bda8..b6d2119 100644 --- a/cv-engine/src/api/export.ts +++ b/cv-engine/src/api/export.ts @@ -10,7 +10,15 @@ export async function exportPdf(cv: CV, endpoint = 'http://localhost:4000/export const response = await axios.post(endpoint, { ...cv, templateId }, { responseType: 'blob' }); const contentDisposition = response.headers['content-disposition'] || ''; const match = /filename="?([^";]+)"?/i.exec(contentDisposition); - const filename = match ? match[1] : `${cv.personal.firstName || 'Candidate'}_${cv.personal.lastName || 'CV'}.pdf`; + const filename = match ? match[1] : (() => { + const today = new Date(); + const y = today.getFullYear(); + const m = String(today.getMonth() + 1).padStart(2, '0'); + const d = String(today.getDate()).padStart(2, '0'); + const dateStr = `${y}${m}${d}`; + const safeLast = (cv.personal.lastName || '').trim() || 'cv'; + return `cv-${safeLast}-${dateStr}.pdf`; + })(); const blob = new Blob([response.data], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); @@ -48,7 +56,14 @@ export async function downloadJob(jobId: string, baseUrl = 'http://localhost:400 const response = await axios.get(`${baseUrl}/export/download/${jobId}`, { responseType: 'blob' }); const contentDisposition = response.headers['content-disposition'] || ''; const match = /filename="?([^";]+)"?/i.exec(contentDisposition); - const filename = match ? match[1] : `CV.pdf`; + const filename = match ? match[1] : (() => { + const today = new Date(); + const y = today.getFullYear(); + const m = String(today.getMonth() + 1).padStart(2, '0'); + const d = String(today.getDate()).padStart(2, '0'); + const dateStr = `${y}${m}${d}`; + return `cv-cv-${dateStr}.pdf`; + })(); const blob = new Blob([response.data], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/cv-engine/src/editors/SkillsEditor.tsx b/cv-engine/src/editors/SkillsEditor.tsx index 86f0337..617dd1b 100644 --- a/cv-engine/src/editors/SkillsEditor.tsx +++ b/cv-engine/src/editors/SkillsEditor.tsx @@ -3,10 +3,19 @@ import { useSkillsData, useCvStore } from '../store/cvStore'; const SkillsEditor: React.FC = () => { const skills = useSkillsData(); - const { addSkill, updateSkill, removeSkill } = useCvStore(); + const { addSkill, updateSkill, removeSkill, reorderSkills } = useCvStore(); const [newSkillName, setNewSkillName] = useState(''); const [newSkillLevel, setNewSkillLevel] = useState<'Beginner' | 'Intermediate' | 'Advanced' | undefined>(undefined); + const levels = ['Beginner', 'Intermediate', 'Advanced'] as const; + type SkillLevel = typeof levels[number] | undefined; + const parseLevel = (value: string): SkillLevel => { + if (!value) return undefined; + return (levels.includes(value as SkillLevel extends undefined ? never : typeof levels[number]) + ? (value as typeof levels[number]) + : undefined); + }; + const handleAddSkill = () => { const name = newSkillName.trim(); if (!name) return; @@ -34,7 +43,7 @@ const SkillsEditor: React.FC = () => { Level (optional) setNewSkillLevel((e.target.value as any) || undefined)} + onChange={(e) => setNewSkillLevel(parseLevel(e.target.value))} className="px-3 py-2 border rounded-md border-gray-300" > None @@ -57,7 +66,7 @@ const SkillsEditor: React.FC = () => { )} - {skills.map((skill) => ( + {skills.map((skill, idx) => ( { /> updateSkill(skill.id, skill.name, (e.target.value as any) || undefined)} + onChange={(e) => updateSkill(skill.id, skill.name, parseLevel(e.target.value))} className="px-3 py-2 border rounded-md border-gray-300" > None @@ -75,6 +84,26 @@ const SkillsEditor: React.FC = () => { Intermediate Advanced + + reorderSkills(idx, Math.max(0, idx - 1))} + disabled={idx === 0} + className={`px-2 py-1 rounded-md text-sm ${idx === 0 ? 'bg-gray-200 text-gray-400' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`} + title="Move up" + > + Up + + reorderSkills(idx, Math.min(skills.length - 1, idx + 1))} + disabled={idx === skills.length - 1} + className={`px-2 py-1 rounded-md text-sm ${idx === skills.length - 1 ? 'bg-gray-200 text-gray-400' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`} + title="Move down" + > + Down + + removeSkill(skill.id)} diff --git a/cv-engine/src/store/cvStore.ts b/cv-engine/src/store/cvStore.ts index ccc6fb7..7a7c972 100644 --- a/cv-engine/src/store/cvStore.ts +++ b/cv-engine/src/store/cvStore.ts @@ -44,6 +44,7 @@ interface CvState { addSkill: (name: string, level?: z.infer['skills'][0]['level']) => void; updateSkill: (id: string, name: string, level?: z.infer['skills'][0]['level']) => void; removeSkill: (id: string) => void; + reorderSkills: (fromIndex: number, toIndex: number) => void; // Navigation setActiveStep: (step: EditorStep) => void; @@ -260,6 +261,17 @@ export const useCvStore = create((set, get) => ({ }); }, + reorderSkills: (fromIndex, toIndex) => { + const { cv } = get(); + const skills = [...cv.skills]; + const [removed] = skills.splice(fromIndex, 1); + skills.splice(toIndex, 0, removed); + set({ + cv: { ...cv, skills }, + isDirty: true + }); + }, + // Navigation setActiveStep: (activeStep) => set({ activeStep }), diff --git a/cv-export-server/server.js b/cv-export-server/server.js index ab13606..2adafb8 100644 --- a/cv-export-server/server.js +++ b/cv-export-server/server.js @@ -204,9 +204,14 @@ app.post('/export/pdf', async (req, res) => { }) : '' }; - const firstName = (sanitizedCv.personal && sanitizedCv.personal.firstName) ? sanitizedCv.personal.firstName : 'Candidate'; - const lastName = (sanitizedCv.personal && sanitizedCv.personal.lastName) ? sanitizedCv.personal.lastName : 'CV'; - const filename = `${firstName}_${lastName}_CV.pdf`.replace(/\s+/g, '_'); + const today = new Date(); + const y = today.getFullYear(); + const m = String(today.getMonth() + 1).padStart(2, '0'); + const d = String(today.getDate()).padStart(2, '0'); + const dateStr = `${y}${m}${d}`; + const rawLast = (sanitizedCv.personal && sanitizedCv.personal.lastName) ? sanitizedCv.personal.lastName : ''; + const safeLast = rawLast.trim() ? rawLast.trim() : 'cv'; + const filename = `cv-${safeLast}-${dateStr}.pdf`.replace(/\s+/g, '_'); if (asyncFlag) { const jobId = jobs.create({ status: 'queued', filename, error: null, buffer: null, templateId });