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