2154512e54
Recipe cards, the recipe detail page, and meal planner still rendered raw
"{n} servings"/"{n}m cook"/"{n} srv" text and untranslated difficulty labels
outside the earlier i18n pass. Also wires the user's locale into AI features
that previously always answered in English regardless of app language:
recipe chat, ingredient substitution, recipe ideas, and generate-from-idea.
The recipe-language picker in the AI generate dialog now defaults to the
user's locale instead of always defaulting to English.
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import Link from "next/link";
|
|
import { useTranslations } from "next-intl";
|
|
import { Clock, Users, Lock, Globe, Link2 } from "lucide-react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
|
|
import { getPublicUrl } from "@/lib/storage";
|
|
|
|
type Recipe = {
|
|
id: string;
|
|
title: string;
|
|
description: string | null;
|
|
baseServings: number;
|
|
prepMins: number | null;
|
|
cookMins: number | null;
|
|
difficulty: "easy" | "medium" | "hard" | null;
|
|
visibility: "private" | "unlisted" | "public";
|
|
updatedAt: Date;
|
|
photos?: Array<{ storageKey: string; isCover: boolean }>;
|
|
};
|
|
|
|
const VISIBILITY_ICON = {
|
|
private: Lock,
|
|
unlisted: Link2,
|
|
public: Globe,
|
|
};
|
|
|
|
const DIFFICULTY_COLOR = {
|
|
easy: "default",
|
|
medium: "secondary",
|
|
hard: "destructive",
|
|
} as const;
|
|
|
|
export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
|
const t = useTranslations("recipe");
|
|
const cover = recipe.photos?.find((p) => p.isCover) ?? recipe.photos?.[0];
|
|
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
|
|
const VisibilityIcon = VISIBILITY_ICON[recipe.visibility];
|
|
|
|
return (
|
|
<Link href={`/recipes/${recipe.id}`}>
|
|
<Card className="group overflow-hidden hover:shadow-md transition-shadow h-full flex flex-col">
|
|
{cover ? (
|
|
<div className="aspect-video overflow-hidden bg-muted">
|
|
<img
|
|
src={getPublicUrl(cover.storageKey)}
|
|
alt={recipe.title}
|
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="aspect-video bg-gradient-to-br from-muted to-muted/50 flex items-center justify-center text-4xl">
|
|
🍽️
|
|
</div>
|
|
)}
|
|
<CardHeader className="pb-2">
|
|
<h3 className="font-semibold leading-tight line-clamp-2 group-hover:text-primary transition-colors">
|
|
{recipe.title}
|
|
</h3>
|
|
{recipe.description && (
|
|
<p className="text-sm text-muted-foreground line-clamp-2">{recipe.description}</p>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent className="pb-2 flex-1" />
|
|
<CardFooter className="pt-4 flex items-center justify-between text-xs text-muted-foreground">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex items-center gap-1">
|
|
<Users className="h-3 w-3" />
|
|
{recipe.baseServings}
|
|
</span>
|
|
{totalMins > 0 && (
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
{t("total", { mins: totalMins })}
|
|
</span>
|
|
)}
|
|
{recipe.difficulty && (
|
|
<Badge variant={DIFFICULTY_COLOR[recipe.difficulty]} className="text-xs h-4">
|
|
{t(`difficulty.${recipe.difficulty}`)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<VisibilityIcon className="h-3 w-3" />
|
|
</CardFooter>
|
|
</Card>
|
|
</Link>
|
|
);
|
|
}
|