e5d1080fb9
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>
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
"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>
|
||
);
|
||
}
|