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 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'; import { normalizeUrl } from '../schema/cvSchema';
const PersonalEditor: React.FC = () => { const PersonalEditor: React.FC = () => {
const personalData = usePersonalData(); const personalData = usePersonalData();
const { updatePersonal } = useCvStore(); const { updatePersonal } = useCvStore();
const templateId = useTemplateId();
const supportsPhoto = !!templatesMap[templateId]?.supportsPhoto;
const [formData, setFormData] = useState(personalData); const [formData, setFormData] = useState(personalData);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
@ -223,7 +226,8 @@ const PersonalEditor: React.FC = () => {
/> />
</div> </div>
{/* Photo URL */} {/* Photo URL (only for templates that support photos) */}
{supportsPhoto && (
<div> <div>
<label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1"> <label htmlFor="photoUrl" className="block text-sm font-medium text-gray-700 mb-1">
Photo URL Photo URL
@ -243,6 +247,7 @@ const PersonalEditor: React.FC = () => {
<p id="photoUrl-error" className="mt-1 text-sm text-red-600">{errors.photoUrl}</p> <p id="photoUrl-error" className="mt-1 text-sm text-red-600">{errors.photoUrl}</p>
)} )}
</div> </div>
)}
</div> </div>
{/* Address Fields */} {/* Address Fields */}

View File

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

View File

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

View File

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

View File

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