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:
parent
d35700bc10
commit
2847cef81b
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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]));
|
||||
Loading…
x
Reference in New Issue
Block a user