diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx index 95fbefb..d03cb82 100644 --- a/app/cart/components/RelatedProducts.tsx +++ b/app/cart/components/RelatedProducts.tsx @@ -132,7 +132,7 @@ export function RelatedProducts({ (opacity: 0) — invisible. Not worth chasing a fix for a scroll-reveal nicety on a list that mutates; a static grid renders correctly with no animation risk. */} -
+
{displayProducts.map((product, i) => { const discount = discountPercent(product.price, product.compareAtPrice); const taxRate = effectiveTaxRate(product, defaultTaxRate); diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx index 5a1d698..c87ae21 100644 --- a/app/components/AddToCartButton.tsx +++ b/app/components/AddToCartButton.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react"; import { addToCart, useCart } from "../lib/cart"; import { useCartFly } from "./CartFly"; -import { NotifyMeForm, NotifyMeFormReservedSpace } from "./NotifyMeForm"; +import { NotifyMeForm } from "./NotifyMeForm"; const FEEDBACK_MS = 2000; @@ -119,23 +119,20 @@ export function AddToCartButton({ ))} )} - {/* Same reserved-space stack as AddToCartInlineButton.tsx's identical - block — see that file's own comment. */} -
-
- -
-
- {currentlyOutOfStock ? ( - 0 ? (selectedVariant ?? "") : ""} /> - ) : ( - - )} -
-
+ + )}
); } diff --git a/app/components/AddToCartInlineButton.tsx b/app/components/AddToCartInlineButton.tsx index cbac8e7..5be25ab 100644 --- a/app/components/AddToCartInlineButton.tsx +++ b/app/components/AddToCartInlineButton.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { addToCart, useCart } from "../lib/cart"; import { useCartFly } from "./CartFly"; -import { NotifyMeForm, NotifyMeFormReservedSpace } from "./NotifyMeForm"; +import { NotifyMeForm } from "./NotifyMeForm"; // Exported so consumers like RelatedProducts.tsx can delay their own // follow-up UI changes (e.g. swapping out this exact card) until after @@ -113,46 +113,36 @@ export function AddToCartInlineButton({ ))} )} - {/* grid + [grid-area:1/1] stack — NotifyMeFormReservedSpace (an - invisible, non-interactive twin of NotifyMeForm's markup) always - contributes its height here, even for an in-stock card that - never shows the real form. Without it, only out-of-stock cards - would be tall enough to need the input+button, and plain CSS - Grid's row-stretch (ProductGrid.tsx has no explicit row height) - would push every sibling card's button down to match whichever - card in the row happens to be out of stock. */} -
-
- -
- {/* self-start, not the stretch default — the button's TOP edge is - what must align across cards (ProductGrid.tsx's flex-1 spacer - pins this whole block to a card's bottom already); the extra - reserved height below a single button just stays blank. */} -
- {currentlyOutOfStock ? ( - 0 ? (selectedVariant ?? "") : ""} /> - ) : ( - - )} -
-
+ {currentlyOutOfStock ? ( + // Replaces the button slot entirely rather than stacking below a + // disabled "Ausverkauft" button. An out-of-stock card is taller + // than its in-stock siblings now — ProductGrid.tsx/MerklisteGrid.tsx/ + // RelatedProducts.tsx all use `items-start` on their grid (not the + // CSS Grid default `stretch`) specifically so that doesn't cascade + // into pushing every other card's button down to match; an earlier + // attempt reserved the extra height invisibly on every card + // instead, which looked worse in practice (visible dead space + // under in-stock cards' buttons). + 0 ? (selectedVariant ?? "") : ""} /> + ) : ( + + )}
); } diff --git a/app/components/NotifyMeForm.tsx b/app/components/NotifyMeForm.tsx index 2409918..9e7525d 100644 --- a/app/components/NotifyMeForm.tsx +++ b/app/components/NotifyMeForm.tsx @@ -8,12 +8,12 @@ import { isValidEmail } from "../lib/email"; * is out of stock — lets a visitor leave their email to be notified once * lib/jobs/sendBackInStockEmails.ts (Payload backend) sends the "it's * back" mail. Always shows the email input + submit button directly (no - * extra click to reveal them) — the resulting taller CTA area is - * reserved on every card via NotifyMeFormReservedSpace below, not just - * the out-of-stock one, so the grid's row-stretch never pushes sibling - * cards' buttons down (see AddToCartInlineButton.tsx/AddToCartButton.tsx's - * own comment on why that reservation lives there). `productId` is the - * numeric Payload id (`product.numericId`), NOT AddToCartInlineButton/ + * extra click to reveal them). The resulting out-of-stock card is taller + * than its in-stock siblings — ProductGrid.tsx/MerklisteGrid.tsx/ + * RelatedProducts.tsx all use `items-start` on their grid so that doesn't + * cascade into pushing every other card's button down to match (see + * AddToCartInlineButton.tsx's own comment). `productId` is the numeric + * Payload id (`product.numericId`), NOT AddToCartInlineButton/ * AddToCartButton's own `id`/`productId` props — those are the commerce * slug (see lib/payload.ts's Product.id comment) — same "numericId, not * id" split WishlistButton already uses. @@ -67,34 +67,19 @@ export function NotifyMeForm({ productId, variantName = "" }: { productId: numbe aria-label="E-Mail-Adresse für Verfügbarkeits-Info" className="w-full rounded-sm border border-border px-3 py-2 text-body-sm text-text-primary bg-bg-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand" /> + {/* Short on purpose — "Bei Verfügbarkeit benachrichtigen" wrapped to + two lines on narrow single-column cards (e.g. /konto/merkliste). + The "Ausverkauft" badge + email field right above already say + what this is for, so "Benachrichtigen" alone reads fine here. */} {error &&

{error}

} ); } -// A non-interactive, visually identical (markup/classes) twin of -// NotifyMeForm's idle state — rendered `invisible` behind every card's -// actual CTA (see the two AddToCartButton components) so an in-stock -// card's own single-button slot reserves the SAME height an out-of-stock -// sibling's input+button would need. Without this, only out-of-stock -// cards would be taller, and plain CSS Grid's row-stretch would then push -// every other card's button down to match — same class of bug as -// ProductGrid.tsx's existing min-h-reserved low-stock line, just for a -// taller block instead of one text line. -export function NotifyMeFormReservedSpace() { - return ( - - ); -} diff --git a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx index 807050d..6a976c7 100644 --- a/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx +++ b/app/email-preview/[type]/components/LiveEmailPreviewClient.tsx @@ -6,6 +6,7 @@ import { renderOrderConfirmationHtml, renderPasswordResetHtml, renderOrderStatusHtml, + renderBackInStockHtml, ORDER_STATUS_EMAIL_ICON, SAMPLE_ORDER, SAMPLE_ORDER_MANUAL, @@ -51,13 +52,15 @@ export function LiveEmailPreviewClient({ ? renderOrderConfirmationHtml(data, orderSample, null) : type === "password-reset" ? renderPasswordResetHtml(data, "https://einfach-produktiv.mk360.de/konto/passwort-zuruecksetzen?token=beispiel-token", null) - : renderOrderStatusHtml( - data, - ORDER_STATUS_EMAIL_ICON[type] ?? "✓", - SAMPLE_ORDER.orderNumber, - `https://einfach-produktiv.mk360.de/konto/bestellungen/${encodeURIComponent(SAMPLE_ORDER.orderNumber)}`, - null, - ); + : type === "back-in-stock" + ? renderBackInStockHtml(data, "ToDo-Karten – Set", "https://einfach-produktiv.mk360.de/todo-cards", null) + : renderOrderStatusHtml( + data, + ORDER_STATUS_EMAIL_ICON[type] ?? "✓", + SAMPLE_ORDER.orderNumber, + `https://einfach-produktiv.mk360.de/konto/bestellungen/${encodeURIComponent(SAMPLE_ORDER.orderNumber)}`, + null, + ); return (
diff --git a/app/email-preview/[type]/page.tsx b/app/email-preview/[type]/page.tsx index 3fd3784..be429b8 100644 --- a/app/email-preview/[type]/page.tsx +++ b/app/email-preview/[type]/page.tsx @@ -42,13 +42,10 @@ const STATUS_TYPE_FALLBACK_HEADING: Record = { // the actual sent email (orderEmail.ts) always reads the published version // instead. // -// "back-in-stock" shares the generic order-status renderer below like -// every other status type — an approximation, not pixel-identical (its -// real send, lib/jobs/sendBackInStockEmails.ts on the Payload side, has no -// order number/link at all, just a product name + "Jetzt ansehen" button), -// same established gap as password-reset's own. Good enough to edit -// subject/heading/bodyText/footerText live, which is what this page is -// actually for. +// "back-in-stock" has its own renderer (renderBackInStockHtml) rather than +// sharing the generic order-status one below — it has no order at all, so +// no order-number line, and its CTA points at the product page ("Zum +// Produkt"), not /konto/bestellungen. export default async function EmailPreviewPage({ params }: { params: Promise<{ type: string }> }) { const { type } = await params; if (!VALID_TYPES.includes(type as EmailTemplateType)) notFound(); diff --git a/app/konto/merkliste/components/MerklisteGrid.tsx b/app/konto/merkliste/components/MerklisteGrid.tsx index 32ae4d9..ccb0b5f 100644 --- a/app/konto/merkliste/components/MerklisteGrid.tsx +++ b/app/konto/merkliste/components/MerklisteGrid.tsx @@ -42,7 +42,7 @@ export function MerklisteGrid({ } return ( - + {visibleEntries.map(({ item, product }) => { const discount = discountPercent(product.price, product.compareAtPrice); const taxRate = effectiveTaxRate(product, defaultTaxRate); diff --git a/app/lib/emailTemplates.ts b/app/lib/emailTemplates.ts index d616570..0a0e5ec 100644 --- a/app/lib/emailTemplates.ts +++ b/app/lib/emailTemplates.ts @@ -361,6 +361,29 @@ export function renderOrderStatusHtml( return emailShell(icon, escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller)); } +// Separate from renderOrderStatusHtml — a back-in-stock mail has no order +// at all (it fires from a "notify me" signup, not a purchase), so no +// "Bestellnummer" line, and the CTA points at the product page, not +// /konto/bestellungen. Mirrors lib/jobs/sendBackInStockEmails.ts on the +// Payload backend, which is where the real send actually happens (see +// this file's own top-of-file comment on why the two aren't literally +// shared code). +export function renderBackInStockHtml(template: EmailTemplateContent, productLabel: string, productUrl: string, seller: CompanySettings | null): string { + const body = ` + ${paragraphs(template.bodyText, "center")} +

${escapeHtml(productLabel)}

+ + + + +
+ Zum Produkt +
+ `; + + return emailShell("🔔", escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller)); +} + export function renderPasswordResetHtml(template: EmailTemplateContent, resetUrl: string, seller: CompanySettings | null): string { const body = ` ${paragraphs(template.bodyText, "center")} diff --git a/app/shop/components/ProductGrid.tsx b/app/shop/components/ProductGrid.tsx index ecc4dc6..10f9cc9 100644 --- a/app/shop/components/ProductGrid.tsx +++ b/app/shop/components/ProductGrid.tsx @@ -122,7 +122,7 @@ export async function ProductGrid({ applying a filter. */} p.id).join(",")} - className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] w-full" + className="grid items-start grid-cols-1 sm:grid-cols-2 lg:grid-cols-12 gap-6 sm:gap-[var(--layout-grid-gap)] w-full" > {products.map((product) => { const discount = discountPercent(product.price, product.compareAtPrice);