115 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|