feat: show recipes forked from the original on the recipe page (v0.82.0)
The "Forked from X" backlink only worked one direction. The original recipe now also shows "Forked by N others" with links to each fork — filtered to forks that are public/unlisted or belong to the viewer, so a private fork never leaks its existence or title to other viewers of the original. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,7 @@ export default async function RecipePage({ params }: Params) {
|
||||
const DIETARY_LABELS = m.recipe.dietary;
|
||||
const unitPref = (session.user as { unitPref?: string }).unitPref === "imperial" ? "imperial" : "metric";
|
||||
|
||||
const [recipe, ratingData, favoriteData, myRating, forkedFrom, myNote, dishCookLog, featureFlags, featurePrefs] = await Promise.all([
|
||||
const [recipe, ratingData, favoriteData, myRating, forkedFrom, forkedTo, myNote, dishCookLog, featureFlags, featurePrefs] = await Promise.all([
|
||||
db.query.recipes.findFirst({
|
||||
where: and(
|
||||
eq(recipes.id, id),
|
||||
@@ -95,6 +95,13 @@ export default async function RecipePage({ params }: Params) {
|
||||
where: eq(recipeVariations.childRecipeId, id),
|
||||
with: { parent: { columns: { id: true, title: true } } },
|
||||
}),
|
||||
db.query.recipeVariations.findMany({
|
||||
where: eq(recipeVariations.parentRecipeId, id),
|
||||
orderBy: (t, { desc }) => desc(t.createdAt),
|
||||
with: {
|
||||
child: { columns: { id: true, title: true, visibility: true, authorId: true } },
|
||||
},
|
||||
}),
|
||||
db.query.recipeNotes.findFirst({
|
||||
where: and(eq(recipeNotes.recipeId, id), eq(recipeNotes.userId, session.user.id)),
|
||||
columns: { content: true },
|
||||
@@ -133,6 +140,13 @@ export default async function RecipePage({ params }: Params) {
|
||||
|
||||
const isOwner = recipe.authorId === session.user.id;
|
||||
|
||||
// Don't leak the existence/title of someone else's private fork —
|
||||
// only surface forks that are themselves public/unlisted, or that the
|
||||
// viewer made themselves.
|
||||
const visibleForks = forkedTo.filter(
|
||||
(f) => f.child.visibility !== "private" || f.child.authorId === session.user.id
|
||||
);
|
||||
|
||||
const dishCookedAtMap = new Map<string, string>();
|
||||
for (const log of dishCookLog) {
|
||||
if (log.batchDishId && !dishCookedAtMap.has(log.batchDishId)) {
|
||||
@@ -194,6 +208,26 @@ export default async function RecipePage({ params }: Params) {
|
||||
{formatMessage(m.recipe.forkedFrom, { title: forkedFrom.parent.title })}
|
||||
</Link>
|
||||
)}
|
||||
{visibleForks.length > 0 && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p>
|
||||
{visibleForks.length === 1
|
||||
? m.recipe.forkedByCountSingular
|
||||
: formatMessage(m.recipe.forkedByCountPlural, { count: visibleForks.length })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-1">
|
||||
{visibleForks.map((f) => (
|
||||
<Link
|
||||
key={f.child.id}
|
||||
href={`/recipes/${f.child.id}`}
|
||||
className="hover:text-foreground underline-offset-2 hover:underline"
|
||||
>
|
||||
{f.child.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-0.5 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
{recipe.steps.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user