Add back-in-stock notification signup form

NotifyMeForm.tsx replaces the disabled Add-to-cart button's spot once
a product/variant is out of stock — POSTs to the new
/api/stock-notifications route, which forwards to the backend's new
stock-notifications collection. Threaded product.numericId (not the
commerce slug id) into AddToCartButton/AddToCartInlineButton for this,
same split WishlistButton already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-08-01 19:48:00 +00:00
parent b0df2f13fa
commit 0dcaad8b8c
11 changed files with 139 additions and 4 deletions
+7
View File
@@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from "react";
import { addToCart, useCart } from "../lib/cart";
import { useCartFly } from "./CartFly";
import { NotifyMeForm } from "./NotifyMeForm";
const FEEDBACK_MS = 2000;
@@ -17,6 +18,7 @@ export function AddToCartButton({
label,
className,
productId = "todo-karten",
numericId,
outOfStock = false,
maxQty = null,
variants = [],
@@ -27,6 +29,10 @@ export function AddToCartButton({
* ProductSpotlight passes the actual CMS-selected spotlight product's id
* explicitly, since that can now be a different product. */
productId?: string;
/** Payload's real numeric product id (`product.numericId`) — only used to
* scope a NotifyMeForm signup once out of stock, never for the cart/
* checkout path itself (that stays on the slug `productId` above). */
numericId: number;
/** Product-level — only meaningful when `variants` is empty, same split as
* AddToCartInlineButton. */
outOfStock?: boolean;
@@ -151,6 +157,7 @@ export function AddToCartButton({
<span className="[grid-area:1/1]">{added ? "Hinzugefügt ✓" : displayLabel}</span>
</span>
</button>
{currentlyOutOfStock && <NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />}
</div>
);
}
+7
View File
@@ -4,6 +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 } 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
@@ -18,6 +19,7 @@ export const FEEDBACK_MS = 2000;
*/
export function AddToCartInlineButton({
id,
numericId,
label = "In den Warenkorb",
className,
outOfStock = false,
@@ -25,6 +27,10 @@ export function AddToCartInlineButton({
variants = [],
}: {
id: string;
/** Payload's real numeric product id (`product.numericId`) — only used to
* scope a NotifyMeForm signup once out of stock, never for the cart/
* checkout path itself (that stays on the slug `id` above). */
numericId: number;
label?: string;
className?: string;
/** Product-level — only meaningful when `variants` is empty. A varianted
@@ -124,6 +130,7 @@ export function AddToCartInlineButton({
</span>
<Image alt="" src="/icon-cart-outline.png" width={32} height={30} className="h-[1.875rem] w-8 object-contain" />
</button>
{currentlyOutOfStock && <NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />}
</div>
);
}
+73
View File
@@ -0,0 +1,73 @@
"use client";
import { useState } from "react";
import { isValidEmail } from "../lib/email";
/**
* Replaces the (disabled) Add-to-cart button's spot once a product/variant
* 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. `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.
*/
export function NotifyMeForm({ productId, variantName = "" }: { productId: number; variantName?: string }) {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
const [error, setError] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!isValidEmail(email)) {
setError("Bitte gib eine gültige E-Mail-Adresse ein.");
return;
}
setError("");
setStatus("submitting");
try {
const res = await fetch("/api/stock-notifications", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, productId, variantName }),
});
const data: { ok: boolean; reason?: string } = await res.json();
if (!data.ok) {
setStatus("error");
setError(data.reason || "Eintragen hat nicht geklappt. Bitte versuch es später erneut.");
return;
}
setStatus("success");
} catch {
setStatus("error");
setError("Eintragen hat nicht geklappt. Bitte versuch es später erneut.");
}
}
if (status === "success") {
return <p className="text-body-sm text-success">Danke! Wir melden uns, sobald es wieder verfügbar ist.</p>;
}
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-2 w-full">
<div className="flex gap-2">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="E-Mail für Verfügbarkeits-Info"
aria-label="E-Mail für Verfügbarkeits-Info"
className="flex-1 min-w-0 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"
/>
<button
type="submit"
disabled={status === "submitting"}
className="shrink-0 rounded-sm border border-border px-4 py-2 text-body-sm font-semibold text-text-primary hover:border-brand transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "submitting" ? "…" : "Benachrichtigen"}
</button>
</div>
{error && <p className="text-label text-red-600">{error}</p>}
</form>
);
}
+1 -1
View File
@@ -127,7 +127,7 @@ export async function ProductSpotlight() {
(matches Tools/Blog above/below), same as AddToCartButton's
own default styling/ring-offset, so no override is needed
here. */}
<AddToCartButton label="In den Warenkorb" productId={product.id} outOfStock={product.outOfStock} maxQty={product.maxQty} variants={product.variants} />
<AddToCartButton label="In den Warenkorb" productId={product.id} numericId={product.numericId} outOfStock={product.outOfStock} maxQty={product.maxQty} variants={product.variants} />
{product.href && (
<Link
href={product.href}