feat: ingredient/tag search on My Recipes, QR code on printed recipes
My Recipes search previously only matched title/description; now also matches ingredient rawName and tags, mirroring the public search improvement from earlier. Hit and fixed a real bug along the way: embedding drizzle column proxies from a foreign table inside a raw sql`` fragment passed to db.query.recipes.findMany's `where` gets rewritten to the wrong table alias by the relational query builder, producing broken SQL — worked fine in the plain query builder used by /api/v1/search, but not here. Fixed by using literal SQL identifiers for the ingredients subquery instead of column proxies. Recipe print pages get the same QR-code treatment shopping lists got earlier, gated on visibility !== "private" (both public and unlisted resolve at /r/[id]). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import QRCode from "qrcode";
|
||||
import { auth } from "@/lib/auth/server";
|
||||
import { db, recipes, eq, and } from "@epicure/db";
|
||||
import { PrintTrigger } from "@/components/recipe/print-trigger";
|
||||
@@ -27,6 +28,13 @@ export default async function RecipePrintPage({ params }: Params) {
|
||||
|
||||
if (!recipe) notFound();
|
||||
|
||||
// /r/[id] resolves for both "public" and "unlisted" (see app/r/[id]/page.tsx)
|
||||
// — only "private" has no scannable link to offer.
|
||||
const shareUrl = recipe.visibility !== "private"
|
||||
? `${process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"}/r/${id}`
|
||||
: null;
|
||||
const qrDataUrl = shareUrl ? await QRCode.toDataURL(shareUrl, { margin: 1, width: 120 }) : null;
|
||||
|
||||
const totalMins = (recipe.prepMins ?? 0) + (recipe.cookMins ?? 0);
|
||||
|
||||
const stepGroups: Array<{ label: string; steps: typeof recipe.steps }> = [];
|
||||
@@ -61,6 +69,10 @@ export default async function RecipePrintPage({ params }: Params) {
|
||||
line-height: 1.6;
|
||||
}
|
||||
h1 { font-size: 2em; margin: 0 0 8px; line-height: 1.2; }
|
||||
.title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
|
||||
.qr-block { text-align: center; flex-shrink: 0; font-family: system-ui, sans-serif; }
|
||||
.qr-block img { width: 84px; height: 84px; display: block; }
|
||||
.qr-block span { display: block; font-size: 0.65em; color: #999; margin-top: 4px; max-width: 100px; }
|
||||
h2 { font-size: 1.1em; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 1px solid #ccc; padding-bottom: 4px; margin: 24px 0 12px; }
|
||||
.meta { display: flex; gap: 24px; font-size: 0.85em; color: #555; margin: 12px 0 16px; flex-wrap: wrap; }
|
||||
.meta span { display: flex; align-items: center; gap: 4px; }
|
||||
@@ -91,7 +103,16 @@ export default async function RecipePrintPage({ params }: Params) {
|
||||
<PrintTrigger />
|
||||
|
||||
<article>
|
||||
<h1>{recipe.title}</h1>
|
||||
<div className="title-row">
|
||||
<h1>{recipe.title}</h1>
|
||||
{qrDataUrl && (
|
||||
<div className="qr-block">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- a data: URL generated server-side, not an optimizable remote image */}
|
||||
<img src={qrDataUrl} alt={m.recipe.qrCodeAlt} />
|
||||
<span>{m.recipe.qrCodeCaption}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{recipe.description && (
|
||||
<p className="description">{recipe.description}</p>
|
||||
|
||||
Reference in New Issue
Block a user