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
167 lines
6.9 KiB
TypeScript
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; |