feat: show an "imported from" badge on recipe cards

sourceUrl was already persisted and shown on the recipe detail page, but
never surfaced on cards in the grid/list/compact views or the simpler
RecipeCard (collections) — added a small external-link icon with a
tooltip naming the source hostname, sized identically to the existing
batch-cook badge so it doesn't reintroduce the compact-view alignment
bug fixed earlier this session.
This commit is contained in:
Arnaud
2026-07-12 16:11:26 +02:00
parent d7e0d7eada
commit e98a9c3bb7
4 changed files with 74 additions and 5 deletions
+6 -2
View File
@@ -3,7 +3,7 @@
import Link from "next/link";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { Clock, Users, Lock, Globe, Link2 } from "lucide-react";
import { Clock, Users, Lock, Globe, Link2, ExternalLink } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { getPublicUrl } from "@/lib/storage";
@@ -19,6 +19,7 @@ type Recipe = {
visibility: "private" | "unlisted" | "public";
updatedAt: Date;
photos?: Array<{ storageKey: string; isCover: boolean }>;
sourceUrl?: string | null;
};
const VISIBILITY_ICON = {
@@ -85,7 +86,10 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
</Badge>
)}
</div>
<VisibilityIcon className="h-3 w-3" />
<div className="flex items-center gap-2 shrink-0">
{recipe.sourceUrl && <ExternalLink className="h-3 w-3" />}
<VisibilityIcon className="h-3 w-3" />
</div>
</CardFooter>
</Card>
</Link>
+34 -1
View File
@@ -3,7 +3,7 @@
import { useState, useCallback, useEffect } from "react";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { Trash2, Globe, Lock, Link2, X, Check, ListChecks, LayoutGrid, List, Rows3, FolderPlus, ChefHat } from "lucide-react";
import { Trash2, Globe, Lock, Link2, X, Check, ListChecks, LayoutGrid, List, Rows3, FolderPlus, ChefHat, ExternalLink } from "lucide-react";
import { AddToCollectionDialog } from "@/components/recipe/add-to-collection-dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import {
@@ -47,6 +47,7 @@ type Recipe = {
isFavorited?: boolean;
isBatchCook?: boolean;
dishCount?: number;
sourceUrl?: string | null;
};
/** Stops the click from bubbling to the card's own Link/select handler. */
@@ -63,6 +64,14 @@ const VIEW_STORAGE_KEY = "epicure-recipes-view";
const DIFFICULTY_COLOR = { easy: "default", medium: "secondary", hard: "destructive" } as const;
const VISIBILITY_ICON = { private: Lock, unlisted: Link2, public: Globe };
function hostnameOf(url: string): string {
try {
return new URL(url).hostname.replace(/^www\./, "");
} catch {
return url;
}
}
function RecipeThumb({ recipe, selectMode, selected, className }: { recipe: Recipe; selectMode: boolean; selected: boolean; className?: string }) {
const cover = recipe.photos?.find((p) => p.isCover) ?? recipe.photos?.[0];
return (
@@ -152,6 +161,14 @@ function GridCard({ recipe, selected, selectMode }: { recipe: Recipe; selected:
)}
</div>
<div className="flex items-center gap-1 shrink-0">
{recipe.sourceUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="p-1.5 inline-flex"><ExternalLink className="h-3.5 w-3.5 text-muted-foreground" /></span>} />
<TooltipContent>{t("importedFrom", { host: hostnameOf(recipe.sourceUrl) })}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{recipe.isBatchCook && (
<TooltipProvider>
<Tooltip>
@@ -199,6 +216,14 @@ function ListRow({ recipe, selected, selectMode }: { recipe: Recipe; selected: b
{recipe.title}
</h3>
<div className="flex items-center gap-1 shrink-0">
{recipe.sourceUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="p-1.5 inline-flex"><ExternalLink className="h-3.5 w-3.5 text-muted-foreground" /></span>} />
<TooltipContent>{t("importedFrom", { host: hostnameOf(recipe.sourceUrl) })}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{recipe.isBatchCook && (
<TooltipProvider>
<Tooltip>
@@ -276,6 +301,14 @@ function CompactRow({ recipe, selected, selectMode }: { recipe: Recipe; selected
<span className="hidden md:inline text-xs text-muted-foreground shrink-0 w-16 text-right">
{recipe.updatedAt.toLocaleDateString(locale, { month: "short", day: "numeric" })}
</span>
{recipe.sourceUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="p-1.5 inline-flex shrink-0"><ExternalLink className="h-3.5 w-3.5 text-muted-foreground" /></span>} />
<TooltipContent>{t("importedFrom", { host: hostnameOf(recipe.sourceUrl) })}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{recipe.isBatchCook && (
<TooltipProvider>
<Tooltip>