feat(templates): add html sanitization for summary sections

Add supportsPhoto flag to template registry and conditionally render photo URL field
This commit is contained in:
geulah 2025-10-11 17:42:05 +01:00
parent d35700bc10
commit 2847cef81b
5 changed files with 37 additions and 28 deletions

View File

@ -1,10 +1,13 @@
import React, { useState, useEffect } from 'react';
import { usePersonalData, useCvStore } from '../store/cvStore';
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<Record<string, string>>({});
@ -223,26 +226,28 @@ const PersonalEditor: React.FC = () => {
/>
</div>
{/* Photo URL */}
<div>
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
Photo URL
</label>
<input
type="url"
id="photoUrl"
name="photoUrl"
placeholder="https://example.com/photo.jpg"
value={formData.photoUrl || ''}
onChange={handleChange}
onBlur={handleBlur}
className={`w-full px-3 py-2 border rounded-md ${errors.photoUrl ? 'border-red-500' : 'border-gray-300'}`}
aria-describedby={errors.photoUrl ? 'photoUrl-error' : undefined}
/>
{errors.photoUrl && (
<p id="photoUrl-error" className="mt-1 text-sm text-red-600">{errors.photoUrl}</p>
)}
</div>
{/* Photo URL (only for templates that support photos) */}
{supportsPhoto && (
<div>
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
Photo URL
</label>
<input
type="url"
id="photoUrl"
name="photoUrl"
placeholder="https://example.com/photo.jpg"
value={formData.photoUrl || ''}
onChange={handleChange}
onBlur={handleBlur}
className={`w-full px-3 py-2 border rounded-md ${errors.photoUrl ? 'border-red-500' : 'border-gray-300'}`}
aria-describedby={errors.photoUrl ? 'photoUrl-error' : undefined}
/>
{errors.photoUrl && (
<p id="photoUrl-error" className="mt-1 text-sm text-red-600">{errors.photoUrl}</p>
)}
</div>
)}
</div>
{/* Address Fields */}

View File

@ -1,5 +1,6 @@
import React from 'react';
import type { CV } from '../schema/cvSchema';
import { sanitizeHtml } from '../schema/cvSchema';
interface ClassicTemplateProps {
cv: CV;
@ -35,7 +36,7 @@ const ClassicTemplate: React.FC<ClassicTemplateProps> = ({ cv }) => {
{summary && (
<section className="mb-4">
<h2 className="text-lg font-medium text-gray-700 mb-2">Professional Summary</h2>
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: summary }} />
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: sanitizeHtml(summary) }} />
</section>
)}

View File

@ -1,5 +1,6 @@
import React from 'react';
import type { CV } from '../schema/cvSchema';
import { sanitizeHtml } from '../schema/cvSchema';
interface MinimalTemplateProps { cv: CV; className?: string; }
@ -25,7 +26,7 @@ const MinimalTemplate: React.FC<MinimalTemplateProps> = ({ cv, className = '' })
{summary && (
<section className="mb-6">
<h2 className="text-xl font-serif mb-2">Summary</h2>
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: summary }} />
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: sanitizeHtml(summary) }} />
</section>
)}

View File

@ -1,5 +1,6 @@
import React from 'react';
import type { CV } from '../schema/cvSchema';
import { sanitizeHtml } from '../schema/cvSchema';
interface ModernTemplateProps { cv: CV; className?: string; }
@ -37,7 +38,7 @@ const ModernTemplate: React.FC<ModernTemplateProps> = ({ cv, className = '' }) =
{summary && (
<section className="mb-6">
<h2 className="text-xl font-semibold text-gray-700 mb-2">Professional Summary</h2>
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: summary }} />
<div className="prose max-w-none" dangerouslySetInnerHTML={{ __html: sanitizeHtml(summary) }} />
</section>
)}

View File

@ -17,13 +17,14 @@ export interface TemplateMeta {
category: TemplateCategory;
thumbnail: string; // path to asset
component: React.FC<{ cv: CV }>;
supportsPhoto?: boolean;
}
export const templatesRegistry: TemplateMeta[] = [
{ id: 'ats', name: 'ATS-Friendly', category: 'ATS', thumbnail: atsThumb, component: ATSTemplate },
{ id: 'classic', name: 'Classic', category: 'Visual', thumbnail: classicThumb, component: ClassicTemplate },
{ id: 'modern', name: 'Modern', category: 'Visual', thumbnail: modernThumb, component: ModernTemplate },
{ id: 'minimal', name: 'Minimal', category: 'Visual', thumbnail: minimalThumb, component: MinimalTemplate },
{ id: 'ats', name: 'ATS-Friendly', category: 'ATS', thumbnail: atsThumb, component: ATSTemplate, supportsPhoto: false },
{ id: 'classic', name: 'Classic', category: 'Visual', thumbnail: classicThumb, component: ClassicTemplate, supportsPhoto: false },
{ id: 'modern', name: 'Modern', category: 'Visual', thumbnail: modernThumb, component: ModernTemplate, supportsPhoto: true },
{ id: 'minimal', name: 'Minimal', category: 'Visual', thumbnail: minimalThumb, component: MinimalTemplate, supportsPhoto: false },
];
export const templatesMap = Object.fromEntries(templatesRegistry.map(t => [t.id, t]));