import React, { useState, useEffect } from 'react'; import { usePersonalData, useCvStore, useTemplateId } from '../store/cvStore'; import { templatesMap } from '../templates/registry'; import { normalizeUrl } from '../schema/cvSchema'; const PersonalEditor: React.FC = () => { const personalData = usePersonalData(); const { updatePersonal } = useCvStore(); const templateId = useTemplateId(); const supportsPhoto = !!templatesMap[templateId]?.supportsPhoto; const [formData, setFormData] = useState(personalData); const [errors, setErrors] = useState>({}); useEffect(() => { setFormData(personalData); }, [personalData]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); // Clear error when user starts typing if (errors[name]) { setErrors(prev => ({ ...prev, [name]: '' })); } }; const handleBlur = (e: React.FocusEvent) => { const { name, value } = e.target; // Validate on blur if (name === 'firstName' && !value.trim()) { setErrors(prev => ({ ...prev, firstName: 'First name is required' })); } else if (name === 'lastName' && !value.trim()) { setErrors(prev => ({ ...prev, lastName: 'Last name is required' })); } else if (name === 'email') { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!value.trim()) { setErrors(prev => ({ ...prev, email: 'Email is required' })); } else if (!emailRegex.test(value)) { setErrors(prev => ({ ...prev, email: 'Invalid email format' })); } } else if (name === 'photoUrl' && value.trim()) { // Basic URL normalization and validation const normalized = normalizeUrl(value.trim()); if (!/^https?:\/\//i.test(normalized)) { setErrors(prev => ({ ...prev, photoUrl: 'URL must start with http:// or https://' })); } else { setFormData(prev => ({ ...prev, photoUrl: normalized })); } } // Update store on blur if no errors if (!Object.values(errors).some(error => error)) { updatePersonal(formData); } }; const handleAddLink = () => { const newLinks = [...formData.links, { id: Math.random().toString(36).substring(2, 9), label: '', url: '' }]; setFormData(prev => ({ ...prev, links: newLinks })); }; const handleLinkChange = (id: string, field: 'label' | 'url', value: string) => { const updatedLinks = formData.links.map(link => link.id === id ? { ...link, [field]: value } : link ); setFormData(prev => ({ ...prev, links: updatedLinks })); // Clear link errors if (errors[`link-${id}-${field}`]) { setErrors(prev => ({ ...prev, [`link-${id}-${field}`]: '' })); } }; const handleLinkBlur = (id: string, field: 'label' | 'url', value: string) => { let hasError = false; if (field === 'url' && value) { try { // Normalize and validate URL const normalizedUrl = normalizeUrl(value); const updatedLinks = formData.links.map(link => link.id === id ? { ...link, url: normalizedUrl } : link ); setFormData(prev => ({ ...prev, links: updatedLinks })); } catch { setErrors(prev => ({ ...prev, [`link-${id}-url`]: 'Invalid URL format' })); hasError = true; } } if (!hasError) { updatePersonal(formData); } }; const handleRemoveLink = (id: string) => { const updatedLinks = formData.links.filter(link => link.id !== id); const updatedFormData = { ...formData, links: updatedLinks }; setFormData(updatedFormData); updatePersonal(updatedFormData); }; // Extras: Headline (extras[0]) and Reference lines (extras[1..]) const handleHeadlineChange = (value: string) => { const extras = Array.isArray(formData.extras) ? [...formData.extras] : []; extras[0] = value; const updatedFormData = { ...formData, extras }; setFormData(updatedFormData); }; const handleReferenceChange = (index: number, value: string) => { const extras = Array.isArray(formData.extras) ? [...formData.extras] : []; extras[index] = value; const updatedFormData = { ...formData, extras }; setFormData(updatedFormData); }; const handleAddReference = () => { const extras = Array.isArray(formData.extras) ? [...formData.extras] : []; extras.push(""); const updatedFormData = { ...formData, extras }; setFormData(updatedFormData); }; const handleRemoveReference = (index: number) => { const extras = Array.isArray(formData.extras) ? [...formData.extras] : []; if (index >= 0 && index < extras.length) { extras.splice(index, 1); } const updatedFormData = { ...formData, extras }; setFormData(updatedFormData); updatePersonal(updatedFormData); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // Validate all fields const newErrors: Record = {}; if (!formData.firstName.trim()) newErrors.firstName = 'First name is required'; if (!formData.lastName.trim()) newErrors.lastName = 'Last name is required'; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!formData.email.trim()) { newErrors.email = 'Email is required'; } else if (!emailRegex.test(formData.email)) { newErrors.email = 'Invalid email format'; } // Validate links formData.links.forEach(link => { if (link.url && !link.url.startsWith('http')) { newErrors[`link-${link.id}-url`] = 'URL must start with http:// or https://'; } }); setErrors(newErrors); // Update store if no errors if (Object.keys(newErrors).length === 0) { updatePersonal(formData); return true; } return false; }; return (

Personal Information

{/* First Name */}
{errors.firstName && (

{errors.firstName}

)}
{/* Last Name */}
{errors.lastName && (

{errors.lastName}

)}
{/* Email */}
{errors.email && (

{errors.email}

)}
{/* Phone */}
{/* Photo URL (only for templates that support photos) */} {supportsPhoto && (
{errors.photoUrl && (

{errors.photoUrl}

)}
)}
{/* Address Fields */}
{/* Links Section */}

Links

{formData.links.map((link) => (
handleLinkChange(link.id, 'label', e.target.value)} onBlur={(e) => handleLinkBlur(link.id, 'label', e.target.value)} className={`w-full px-3 py-2 border rounded-md ${ errors[`link-${link.id}-label`] ? 'border-red-500' : 'border-gray-300' }`} /> {errors[`link-${link.id}-label`] && (

{errors[`link-${link.id}-label`]}

)}
handleLinkChange(link.id, 'url', e.target.value)} onBlur={(e) => handleLinkBlur(link.id, 'url', e.target.value)} className={`w-full px-3 py-2 border rounded-md ${ errors[`link-${link.id}-url`] ? 'border-red-500' : 'border-gray-300' }`} /> {errors[`link-${link.id}-url`] && (

{errors[`link-${link.id}-url`]}

)}
))}
{/* Headline & Reference Section */}

Headline & Reference

{/* Headline */}
handleHeadlineChange(e.target.value)} onBlur={() => updatePersonal(formData)} className="w-full px-3 py-2 border border-gray-300 rounded-md" />
{/* Reference Lines */}

Reference Lines (left sidebar)

{(formData.extras || []).slice(1).map((line, i) => (
handleReferenceChange(i + 1, e.target.value)} onBlur={() => updatePersonal(formData)} className="w-full px-3 py-2 border border-gray-300 rounded-md" />
))}
); }; export default PersonalEditor;