Files
Epicure/apps/web/components/recipe/version-diff-view.tsx
T
Arnaud e5d1080fb9 feat: implement remaining TODO.md feature ideas + fix mobile headers
Implements the six previously-unscoped feature ideas plus a mobile
layout fix reported via screenshot:

- Mobile: Recipes/Collections/Pantry/Meal Plan/Shopping Lists headers
  now stack and wrap instead of clipping buttons on narrow viewports.
- Recipe diff/compare view: word/list diff against any past version,
  next to Restore in version history.
- Shared meal plans & shopping lists: new shoppingListMembers/
  mealPlanMembers tables (viewer/editor roles, mirrors
  collectionMembers), share dialogs, membership-checked routes.
- PDF cookbook export: /print/collection/[id] renders a whole
  collection with page breaks, using the existing print-CSS pattern
  instead of adding a PDF rendering dependency.
- Grocery delivery handoff: shopping lists can copy-as-text (works
  today) or send to Instacart once INSTACART_API_KEY is configured
  (stub adapter — real API needs a partner agreement).
- Personalized "For You" feed tab: ranks public recipes by tag/
  dietary overlap with the user's favorited/highly-rated history.
- PWA: added manifest.json + icons on top of the existing service
  worker so the app is installable; cook-mode pages were already
  cached for offline use.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 12:13:00 +02:00

104 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { diffWords } from "diff";
export type DiffSnapshot = {
title: string;
description?: string | null;
ingredients: Array<{ rawName: string; quantity?: string | number | null; unit?: string | null; note?: string | null }>;
steps: Array<{ instruction: string; timerSeconds?: number | null }>;
};
function ingredientLine(i: DiffSnapshot["ingredients"][number]): string {
return [i.quantity, i.unit, i.rawName, i.note && `(${i.note})`].filter(Boolean).join(" ");
}
function stepLine(s: DiffSnapshot["steps"][number]): string {
return s.instruction;
}
function TextDiff({ before, after }: { before: string; after: string }) {
const parts = diffWords(before, after);
return (
<p className="text-sm leading-relaxed">
{parts.map((part, i) => (
<span
key={i}
className={
part.added
? "bg-green-500/20 text-green-700 dark:text-green-400"
: part.removed
? "bg-red-500/20 text-red-700 dark:text-red-400 line-through"
: undefined
}
>
{part.value}
</span>
))}
</p>
);
}
function ListDiff({ before, after }: { before: string[]; after: string[] }) {
const max = Math.max(before.length, after.length);
const rows = [];
for (let i = 0; i < max; i++) {
const b = before[i];
const a = after[i];
if (b === undefined) {
rows.push(
<div key={i} className="bg-green-500/10 rounded px-2 py-1 text-sm text-green-700 dark:text-green-400">
+ {a}
</div>
);
} else if (a === undefined) {
rows.push(
<div key={i} className="bg-red-500/10 rounded px-2 py-1 text-sm text-red-700 dark:text-red-400 line-through">
{b}
</div>
);
} else if (b === a) {
rows.push(
<div key={i} className="px-2 py-1 text-sm text-muted-foreground">
{b}
</div>
);
} else {
rows.push(
<div key={i} className="rounded px-2 py-1">
<TextDiff before={b} after={a} />
</div>
);
}
}
return <div className="space-y-1">{rows}</div>;
}
export function VersionDiffView({ before, after }: { before: DiffSnapshot; after: DiffSnapshot }) {
return (
<div className="space-y-6">
<section className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Title</h3>
<TextDiff before={before.title} after={after.title} />
</section>
{(before.description || after.description) && (
<section className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Description</h3>
<TextDiff before={before.description ?? ""} after={after.description ?? ""} />
</section>
)}
<section className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Ingredients</h3>
<ListDiff before={before.ingredients.map(ingredientLine)} after={after.ingredients.map(ingredientLine)} />
</section>
<section className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Steps</h3>
<ListDiff before={before.steps.map(stepLine)} after={after.steps.map(stepLine)} />
</section>
</div>
);
}