feat: recipe detail + weekly shopping list (PRD §5.1, §5.4)
Recipe detail - Seed catalog of 33 italian ingredients with macros, allergens, avg price. - Each seed recipe now declares its RecipeIngredient links (qty + unit). - /recipes/[id] page: editorial layout with prep meta, ingredients list, macros block, preparation, chef's note. Plan-week meal names linkable. Shopping list - GET /me/shopping-list aggregates RecipeIngredient quantities of the ACTIVE plan, grouped by ingredient category, sorted alphabetically. - /shopping page: total items, estimated cost, per-category checklist with check-off persisted in localStorage per plan id. - Home gets a "Spesa" nav item; /plan links the list when a plan exists. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b79340efb7
commit
ad1e4ecaeb
13 files changed
+1331
-14
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { prisma } from '@ketopath/db';
|
||||
import Link from 'next/link';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { setRequestLocale } from 'next-intl/server';
|
||||
@@ -62,12 +63,20 @@ function PlanPageContent({ plan }: { plan: Awaited<ReturnType<typeof fetchCurren
|
||||
{plan ? formatWeek(plan.weekStart) : t('noPlan')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="animate-fade-up [animation-delay:360ms] md:col-span-4 md:text-right">
|
||||
<div className="animate-fade-up flex flex-col items-stretch gap-3 [animation-delay:360ms] md:col-span-4 md:items-end">
|
||||
<form action={regeneratePlan}>
|
||||
<Button type="submit" variant={plan ? 'outline' : 'default'} size="lg">
|
||||
{plan ? t('regenerate') : t('generate')}
|
||||
</Button>
|
||||
</form>
|
||||
{plan ? (
|
||||
<Link
|
||||
href="/shopping"
|
||||
className="text-ink-soft decoration-pomodoro hover:text-ink font-mono text-[11px] uppercase tracking-widest underline decoration-[1.5px] underline-offset-[5px] transition-colors"
|
||||
>
|
||||
Lista della spesa →
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
@@ -87,9 +88,16 @@ function SlotCard({ slot, index }: { slot: PlanSlot; index: number }) {
|
||||
<span>{t(`meals.${slot.meal}`)}</span>
|
||||
</p>
|
||||
</header>
|
||||
<p className="font-display text-ink text-lg font-medium leading-tight">
|
||||
{slot.selected?.name ?? t('noRecipe')}
|
||||
</p>
|
||||
{slot.selected ? (
|
||||
<Link
|
||||
href={`/recipes/${slot.selected.id}`}
|
||||
className="font-display text-ink decoration-pomodoro hover:decoration-ink text-lg font-medium leading-tight underline decoration-[1.5px] underline-offset-[5px] transition-colors"
|
||||
>
|
||||
{slot.selected.name}
|
||||
</Link>
|
||||
) : (
|
||||
<p className="font-display text-ink-soft text-lg italic leading-tight">{t('noRecipe')}</p>
|
||||
)}
|
||||
{slot.selected ? (
|
||||
<p className="text-ink-soft font-mono text-xs tracking-tight">
|
||||
{Math.round(slot.selected.kcal)}{' '}
|
||||
@@ -109,12 +117,15 @@ function SlotCard({ slot, index }: { slot: PlanSlot; index: number }) {
|
||||
<ul className="border-ink/15 mt-3 space-y-2 border-t pt-3">
|
||||
{slot.alternatives.map((alt) => (
|
||||
<li key={alt.id} className="flex items-baseline justify-between gap-2">
|
||||
<span className="font-display text-ink text-sm leading-snug">
|
||||
<Link
|
||||
href={`/recipes/${alt.id}`}
|
||||
className="font-display text-ink hover:text-pomodoro text-sm leading-snug transition-colors"
|
||||
>
|
||||
{alt.name}{' '}
|
||||
<span className="text-ink-dim font-mono text-[10px]">
|
||||
{Math.round(alt.kcal)} kcal
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
|
||||
Reference in new issue
Block a user