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>
This commit is contained in:
Arnaud
2026-07-02 12:13:00 +02:00
parent d2faf98ac1
commit e5d1080fb9
56 changed files with 6096 additions and 74 deletions
@@ -12,16 +12,24 @@ type ShoppingListItem = {
checkedItems: number;
};
type Props = {
lists: ShoppingListItem[];
type SharedListItem = {
id: string;
name: string;
ownerName: string;
role: "viewer" | "editor";
};
export function ShoppingListsPageContent({ lists }: Props) {
type Props = {
lists: ShoppingListItem[];
sharedLists?: SharedListItem[];
};
export function ShoppingListsPageContent({ lists, sharedLists = [] }: Props) {
const t = useTranslations("shoppingLists");
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-muted-foreground mt-1">{t("subtitle")}</p>
@@ -58,6 +66,26 @@ export function ShoppingListsPageContent({ lists }: Props) {
))}
</div>
)}
{sharedLists.length > 0 && (
<div className="space-y-3 max-w-lg">
<h2 className="text-sm font-semibold text-muted-foreground">Shared with you</h2>
{sharedLists.map((list) => (
<Link
key={list.id}
href={`/shopping-lists/${list.id}`}
className="flex items-center justify-between rounded-xl border p-4 hover:shadow-sm transition-shadow"
>
<div className="space-y-1">
<h3 className="font-semibold">{list.name}</h3>
<p className="text-sm text-muted-foreground">
{list.ownerName} · {list.role}
</p>
</div>
</Link>
))}
</div>
)}
</div>
);
}