127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
import { headers } from "next/headers";
|
||
import { auth } from "@/lib/auth/server";
|
||
import { db, mealPlans, eq, and } from "@epicure/db";
|
||
import { PrintTrigger } from "@/components/recipe/print-trigger";
|
||
|
||
const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
|
||
const DAY_LABELS: Record<string, string> = {
|
||
mon: "Monday", tue: "Tuesday", wed: "Wednesday", thu: "Thursday",
|
||
fri: "Friday", sat: "Saturday", sun: "Sunday",
|
||
};
|
||
const MEAL_ORDER = ["breakfast", "lunch", "dinner", "snack"] as const;
|
||
const MEAL_LABELS: Record<string, string> = {
|
||
breakfast: "Breakfast", lunch: "Lunch", dinner: "Dinner", snack: "Snack",
|
||
};
|
||
|
||
function getMonday(dateStr?: string): Date {
|
||
const d = dateStr ? new Date(dateStr) : new Date();
|
||
const day = d.getDay();
|
||
const diff = day === 0 ? -6 : 1 - day;
|
||
d.setDate(d.getDate() + diff);
|
||
d.setHours(0, 0, 0, 0);
|
||
return d;
|
||
}
|
||
|
||
export default async function MealPlanPrintPage({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<{ week?: string }>;
|
||
}) {
|
||
const { week } = await searchParams;
|
||
const session = await auth.api.getSession({ headers: await headers() });
|
||
if (!session) return null;
|
||
|
||
const monday = getMonday(week);
|
||
const weekStart = monday.toISOString().slice(0, 10);
|
||
const sunday = new Date(monday);
|
||
sunday.setDate(sunday.getDate() + 6);
|
||
|
||
const plan = await db.query.mealPlans.findFirst({
|
||
where: and(eq(mealPlans.userId, session.user.id), eq(mealPlans.weekStart, weekStart)),
|
||
with: { entries: { with: { recipe: true } } },
|
||
});
|
||
|
||
const label = `${monday.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${sunday.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}`;
|
||
|
||
const entries = plan?.entries ?? [];
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
@media print {
|
||
body { margin: 0; }
|
||
.no-print { display: none !important; }
|
||
}
|
||
body {
|
||
font-family: system-ui, sans-serif;
|
||
color: #1a1a1a;
|
||
max-width: 900px;
|
||
margin: 40px auto;
|
||
padding: 0 24px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
h1 { font-size: 1.8em; margin: 0 0 2px; }
|
||
.subtitle { color: #666; font-size: 0.9em; margin-bottom: 24px; }
|
||
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||
th { text-align: left; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.06em; color: #888; padding: 6px 8px; border-bottom: 2px solid #ccc; }
|
||
td { padding: 6px 8px; border-bottom: 1px solid #eee; border-right: 1px solid #eee; vertical-align: top; min-height: 40px; }
|
||
td:last-child { border-right: none; }
|
||
.meal-label { font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.05em; color: #aaa; display: block; margin-bottom: 2px; }
|
||
.recipe-title { font-weight: 500; }
|
||
.servings { font-size: 0.8em; color: #888; }
|
||
.empty { color: #ccc; font-size: 0.85em; }
|
||
footer { margin-top: 40px; font-size: 0.75em; color: #aaa; text-align: center; }
|
||
.print-btn {
|
||
position: fixed; top: 16px; right: 16px; padding: 8px 16px;
|
||
background: #18181b; color: white; border: none; border-radius: 6px;
|
||
cursor: pointer; font-size: 13px;
|
||
}
|
||
.print-btn:hover { background: #3f3f46; }
|
||
`}</style>
|
||
|
||
<PrintTrigger />
|
||
|
||
<h1>Meal Plan</h1>
|
||
<p className="subtitle">{label}</p>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style={{ width: "100px" }}>Day</th>
|
||
{MEAL_ORDER.map((m) => (
|
||
<th key={m}>{MEAL_LABELS[m]}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{DAYS.map((day) => (
|
||
<tr key={day}>
|
||
<td style={{ fontWeight: 600, color: "#555" }}>{DAY_LABELS[day]}</td>
|
||
{MEAL_ORDER.map((mealType) => {
|
||
const entry = entries.find((e) => e.day === day && e.mealType === mealType);
|
||
return (
|
||
<td key={mealType}>
|
||
{entry ? (
|
||
<>
|
||
<span className="recipe-title">{entry.recipe?.title ?? entry.note ?? "—"}</span>
|
||
{entry.servings && (
|
||
<span className="servings"> · {entry.servings} srv</span>
|
||
)}
|
||
</>
|
||
) : (
|
||
<span className="empty">—</span>
|
||
)}
|
||
</td>
|
||
);
|
||
})}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
<footer>Printed from Epicure</footer>
|
||
</>
|
||
);
|
||
}
|