CV-Engine/cv-engine/src/editors/TemplateSelectionEditor.tsx
geulah 3a9591fb48 feat(templates): add color theme support and template selection step
Implement color theme system with 9 color options and default theme
Add template selection as first step in CV creation flow
Update all templates to support dynamic color theming
Create ThemeProvider component to apply theme styles
Add template thumbnails with color theme variations
Extend CV schema with colorTheme field
Update store to handle template selection and color theme state
2025-10-14 22:39:36 +01:00

167 lines
6.9 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { templatesRegistry, getTemplateThumbnail, type TemplateCategory } from '../templates/registry';
import { useCvStore, useColorTheme } from '../store/cvStore';
import { colorThemes, colorThemeOrder, type ColorTheme } from '../types/colors';
const TemplateSelectionEditor: React.FC = () => {
const templateId = useCvStore(state => state.cv.templateId);
const updateTemplateId = useCvStore(state => state.updateTemplateId);
const colorTheme = useColorTheme();
const updateColorTheme = useCvStore(state => state.updateColorTheme);
const [filter, setFilter] = useState<'All' | TemplateCategory>('All');
const filtered = useMemo(() => {
return templatesRegistry.filter(t => filter === 'All' ? true : t.category === filter);
}, [filter]);
const handleTemplateSelect = (id: string) => {
updateTemplateId(id);
};
const handleColorSelect = (color: ColorTheme) => {
updateColorTheme(color);
};
return (
<div className="bg-white rounded-lg shadow-sm p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
What do you want your CV to look like?
</h1>
<p className="text-gray-600">
Scroll to view all styles and click to select a specific style.
</p>
</div>
{/* Color selection */}
<div className="flex justify-center gap-2 mb-8">
{colorThemeOrder.map(color => {
if (color === null) {
// Default/None option
return (
<button
key="default"
type="button"
onClick={() => handleColorSelect(null)}
className={`w-6 h-6 rounded-full border-2 border-gray-300 bg-white transition-all duration-200 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center justify-center ${
colorTheme === null
? 'ring-2 ring-offset-2 ring-gray-400 scale-110'
: 'hover:ring-1 hover:ring-offset-1 hover:ring-gray-300'
}`}
aria-label="Select default color theme"
>
<span className="text-xs text-gray-500 font-medium">/</span>
</button>
);
}
const palette = colorThemes[color];
return (
<button
key={color}
type="button"
onClick={() => handleColorSelect(color)}
className={`w-6 h-6 rounded-full transition-all duration-200 hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
colorTheme === color
? 'ring-2 ring-offset-2 ring-gray-400 scale-110'
: 'hover:ring-1 hover:ring-offset-1 hover:ring-gray-300'
}`}
style={{ backgroundColor: palette.primary }}
aria-label={`Select ${color} color theme`}
/>
);
})}
</div>
{/* Filter buttons */}
<div className="flex justify-center gap-4 mb-8">
{(['All', 'ATS', 'Visual'] as const).map(category => (
<button
key={category}
type="button"
onClick={() => setFilter(category)}
className={`px-6 py-2 rounded-full text-sm font-medium transition-colors ${
filter === category
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{category}
</button>
))}
</div>
{/* Template grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6 max-w-7xl mx-auto">
{filtered.map(template => (
<div key={template.id} className="flex flex-col items-center">
<button
type="button"
onClick={() => handleTemplateSelect(template.id)}
className={`group relative w-full aspect-[3/4] border-2 rounded-lg overflow-hidden transition-all duration-200 hover:shadow-lg ${
templateId === template.id
? 'border-blue-600 ring-2 ring-blue-600 ring-opacity-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
{/* Template thumbnail */}
<div className="w-full h-full bg-gray-50 flex items-center justify-center">
<img
src={getTemplateThumbnail(template.id, colorTheme)}
alt={`${template.name} template preview`}
className="w-full h-full object-contain"
/>
</div>
{/* Selection indicator */}
{templateId === template.id && (
<div className="absolute top-2 right-2 w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
)}
</button>
{/* Template info */}
<div className="mt-3 text-center">
<h3 className="font-semibold text-gray-900">{template.name}</h3>
{template.category === 'ATS' && (
<span className="inline-block mt-1 px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
Recommended
</span>
)}
{template.category === 'Visual' && template.name === 'Smart' && (
<span className="inline-block mt-1 px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
Recommended
</span>
)}
{template.category === 'Visual' && template.name === 'Traditional 2' && (
<span className="inline-block mt-1 px-2 py-1 text-xs bg-purple-100 text-purple-800 rounded-full">
Recommended
</span>
)}
</div>
</div>
))}
</div>
{/* Additional templates section */}
<div className="mt-12 text-center">
<p className="text-gray-500 text-sm mb-4">
Need more options? Additional templates are available
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-2xl mx-auto opacity-60">
{/* Placeholder for additional templates */}
{[1, 2, 3, 4].map(i => (
<div key={i} className="aspect-[3/4] bg-gray-100 rounded-lg border-2 border-dashed border-gray-300 flex items-center justify-center">
<span className="text-gray-400 text-xs">Coming Soon</span>
</div>
))}
</div>
</div>
</div>
);
};
export default TemplateSelectionEditor;