diff --git a/apps/web/app/(app)/recipes/page.tsx b/apps/web/app/(app)/recipes/page.tsx
index 215bd45..fec4ec2 100644
--- a/apps/web/app/(app)/recipes/page.tsx
+++ b/apps/web/app/(app)/recipes/page.tsx
@@ -2,8 +2,8 @@ import type { Metadata } from "next";
import { headers } from "next/headers";
import Link from "next/link";
import { auth } from "@/lib/auth/server";
-import { db, recipes, sql, count } from "@epicure/db";
-import { eq, desc, asc, and, ilike, or } from "@epicure/db";
+import { db, recipes, favorites, sql, count } from "@epicure/db";
+import { eq, desc, asc, and, ilike, or, inArray } from "@epicure/db";
import { RecipesHeader } from "@/components/recipe/recipes-header";
import { RecipesEmptyState } from "@/components/recipe/recipes-empty-state";
import { RecipesGrid } from "@/components/recipe/recipes-grid";
@@ -76,6 +76,16 @@ export default async function RecipesPage({ searchParams }: { searchParams: Sear
const total = totalRow[0]?.count ?? 0;
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+ const recipeIds = userRecipes.map((r) => r.id);
+ const favoritedRows = recipeIds.length > 0
+ ? await db
+ .select({ recipeId: favorites.recipeId })
+ .from(favorites)
+ .where(and(eq(favorites.userId, session.user.id), inArray(favorites.recipeId, recipeIds)))
+ : [];
+ const favoritedIds = new Set(favoritedRows.map((r) => r.recipeId));
+ const recipesWithFavorites = userRecipes.map((r) => ({ ...r, isFavorited: favoritedIds.has(r.id) }));
+
const pageHref = (p: number) => {
const params = new URLSearchParams();
if (query) params.set("q", query);
@@ -105,7 +115,7 @@ export default async function RecipesPage({ searchParams }: { searchParams: Sear
initialTag={tagFilter ?? ""}
/>
-
+
{totalPages > 1 && (
diff --git a/apps/web/components/recipe/recipes-grid.tsx b/apps/web/components/recipe/recipes-grid.tsx
index 0478176..f4317ee 100644
--- a/apps/web/components/recipe/recipes-grid.tsx
+++ b/apps/web/components/recipe/recipes-grid.tsx
@@ -23,6 +23,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { FavoriteButton } from "@/components/social/favorite-button";
import { toast } from "sonner";
import { getPublicUrl } from "@/lib/storage";
import { useLocale } from "@/lib/i18n/provider";
@@ -43,8 +44,14 @@ type Recipe = {
tags: string[];
updatedAt: Date;
photos?: Array<{ storageKey: string; isCover: boolean }>;
+ isFavorited?: boolean;
};
+/** Stops the click from bubbling to the card's own Link/select handler. */
+function StopPropagation({ children }: { children: React.ReactNode }) {
+ return
e.stopPropagation()}>{children}
;
+}
+
type ViewMode = "grid" | "list" | "compact";
const VIEW_STORAGE_KEY = "epicure-recipes-view";
@@ -103,6 +110,13 @@ function GridCard({ recipe, selected, selectMode }: { recipe: Recipe; selected:
)}
>
+ {!selectMode && (
+
+
+
+
+
+ )}
{recipe.title}
@@ -155,7 +169,14 @@ function ListRow({ recipe, selected, selectMode }: { recipe: Recipe; selected: b
{recipe.title}
-
+
+ {!selectMode && (
+
+
+
+ )}
+
+
{recipe.description &&
{recipe.description}
}
@@ -198,6 +219,11 @@ function CompactRow({ recipe, selected, selectMode }: { recipe: Recipe; selected
{recipe.updatedAt.toLocaleDateString(locale, { month: "short", day: "numeric" })}
+ {!selectMode && (
+
+
+
+ )}
);
@@ -403,14 +429,14 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
{/* Floating bulk action bar */}
{selectMode && selected.size > 0 && (
-
-
-
{t("selectedCount", { count: selected.size })}
-
+
+
+
{selected.size}
+
-
+
- {t("visibilityMenuLabel")}
+ {t("visibilityMenuLabel")}
{ void bulkSetVisibility("public"); }}>
@@ -424,13 +450,13 @@ export function RecipesGrid({ recipes: initialRecipes }: { recipes: Recipe[] })
-
diff --git a/apps/web/components/social/favorite-button.tsx b/apps/web/components/social/favorite-button.tsx
index a3a74ae..b898e8f 100644
--- a/apps/web/components/social/favorite-button.tsx
+++ b/apps/web/components/social/favorite-button.tsx
@@ -12,11 +12,14 @@ export function FavoriteButton({
recipeId,
initialFavorited = false,
onToggle,
+ iconClassName,
}: {
recipeId: string;
initialFavorited?: boolean;
/** Called after a successful toggle, e.g. to remove the recipe from a "Favorites" list view. */
onToggle?: (favorited: boolean) => void;
+ /** Extra classes for the heart icon when unfavorited — e.g. forcing a light color when the button overlays a photo. */
+ iconClassName?: string;
}) {
const tCommon = useTranslations("common");
const tSocial = useTranslations("social");
@@ -52,7 +55,7 @@ export function FavoriteButton({
disabled={loading}
aria-label={favorited ? tSocial("favoriteRemove") : tSocial("favoriteAdd")}
>
-
+
} />
{favorited ? "Saved" : "Save"}