recipe-manager/frontend/src/components/RecipeCard.tsx

115 lines
5.0 KiB
TypeScript

import { Link } from 'react-router-dom';
import type { Recipe, Tag } from '../types/recipe';
import { colors, radius, recipeAccentPalette, shadows, typography } from '../theme';
interface RecipeCardProps {
recipe: Recipe;
tags?: Tag[];
}
const foodEmojis = ['🥗', '🍲', '🍝', '🍜', '🍛', '🥘', '🍗', '🍤', '🍕', '🥪', '🍳', '🍱'];
function formatTime(minutes?: number): string {
if (!minutes) return '';
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
}
function formatDate(timestamp?: number): string {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
return date.toLocaleDateString();
}
function accentForRecipe(recipe: Recipe, tags: Tag[]) {
if (tags[0]?.color) return tags[0].color;
return recipeAccentPalette[recipe.id % recipeAccentPalette.length];
}
function emojiForRecipe(recipe: Recipe) {
return foodEmojis[recipe.id % foodEmojis.length];
}
function MetaChip({ label, value, emoji }: { label: string; value: string; emoji: string }) {
return (
<div className="inline-flex items-center gap-1.5 rounded-full border border-slate-200/80 bg-slate-50 px-2.5 py-1 text-xs text-slate-700 transition-all duration-200 group-hover:border-slate-300 group-hover:bg-white">
<span aria-hidden="true">{emoji}</span>
<span className="font-medium">{value}</span>
<span className="text-slate-500">{label}</span>
</div>
);
}
export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) {
const totalTime = (recipe.prep_time_minutes || 0) + (recipe.cook_time_minutes || 0);
const accent = accentForRecipe(recipe, tags);
return (
<Link
to={`/recipe/${recipe.id}`}
className="group block overflow-hidden border border-slate-200/80 bg-white/95 outline-none transition-all duration-200 hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
style={{ boxShadow: shadows.card, borderRadius: radius.lg }}
>
<div
className="relative h-40 border-b border-slate-200/70"
style={{
background: `linear-gradient(135deg, color-mix(in srgb, ${accent} 30%, white) 0%, color-mix(in srgb, ${accent} 14%, #f8fafc) 55%, #ffffff 100%)`,
}}
>
<div className="absolute left-4 top-4 rounded-full border border-white/70 bg-white/80 px-3 py-1 text-xs font-semibold text-slate-700 backdrop-blur-sm transition-transform duration-200 group-hover:scale-[1.02]">
{tags[0]?.name ?? 'Homemade'}
</div>
<div className="absolute inset-0 flex items-center justify-center text-6xl opacity-90 transition-transform duration-300 group-hover:scale-105" aria-hidden="true">
{emojiForRecipe(recipe)}
</div>
</div>
<div className="flex h-[calc(100%-10rem)] min-h-[210px] flex-col p-5">
<h3
className="mb-1 line-clamp-2 text-lg font-bold text-gray-900 transition-colors duration-200 group-hover:text-blue-700"
style={{ fontSize: typography.fontSize.lg }}
>
{recipe.title}
</h3>
{recipe.description ? (
<p className="mb-3 line-clamp-2 text-sm text-gray-600">{recipe.description}</p>
) : (
<p className="mb-3 text-sm italic text-gray-400">No description yet</p>
)}
<div className="mb-3 flex flex-wrap gap-2">
{recipe.servings ? <MetaChip emoji="🍽️" value={`${recipe.servings}`} label="servings" /> : null}
{totalTime > 0 ? <MetaChip emoji="⏱️" value={formatTime(totalTime)} label="total" /> : null}
<MetaChip emoji="🥄" value={`${recipe.ingredients.length}`} label="ingredients" />
</div>
{tags.length > 0 && (
<div className="mb-3 flex flex-wrap gap-1.5">
{tags.slice(0, 4).map((tag) => (
<span
key={tag.id}
className="rounded-full px-2.5 py-1 text-xs font-semibold text-white shadow-sm transition-transform duration-200 group-hover:scale-[1.02]"
style={{ backgroundColor: tag.color || colors.primary, borderRadius: radius.full }}
>
{tag.name}
</span>
))}
{tags.length > 4 ? <span className="rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">+{tags.length - 4}</span> : null}
</div>
)}
<div className="mt-auto flex items-center justify-between border-t border-slate-100 pt-3 text-xs text-gray-500">
<span>{recipe.last_cooked_at ? `Cooked ${formatDate(recipe.last_cooked_at)}` : 'Not cooked yet'}</span>
<span className="inline-flex items-center gap-1 font-medium text-blue-600">
View recipe
<span className="transition-transform duration-200 group-hover:translate-x-0.5" aria-hidden="true"></span>
</span>
</div>
</div>
</Link>
);
}