diff --git a/app/api/stock-notifications/route.ts b/app/api/stock-notifications/route.ts
new file mode 100644
index 0000000..f335c28
--- /dev/null
+++ b/app/api/stock-notifications/route.ts
@@ -0,0 +1,21 @@
+import { NextResponse } from "next/server";
+import { isValidEmail } from "../../lib/email";
+import { createStockNotification } from "../../lib/stockNotifications";
+
+export async function POST(request: Request) {
+ const body = await request.json().catch(() => null);
+ const email = typeof body?.email === "string" ? body.email.trim() : "";
+ const productId = Number(body?.productId);
+ const variantName = typeof body?.variantName === "string" ? body.variantName : "";
+
+ if (!isValidEmail(email)) {
+ return NextResponse.json({ ok: false, reason: "Bitte gib eine gültige E-Mail-Adresse ein." }, { status: 400 });
+ }
+ if (!Number.isInteger(productId) || productId <= 0) {
+ return NextResponse.json({ ok: false, reason: "Ungültiges Produkt." }, { status: 400 });
+ }
+
+ const result = await createStockNotification(email, productId, variantName);
+ if (!result.ok) return NextResponse.json(result, { status: 500 });
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx
index a817aaf..95fbefb 100644
--- a/app/cart/components/RelatedProducts.tsx
+++ b/app/cart/components/RelatedProducts.tsx
@@ -221,7 +221,7 @@ export function RelatedProducts({
lines (see ProductGrid.tsx's identical spacer). */}
-
+
);
diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx
index ced7908..296edff 100644
--- a/app/components/AddToCartButton.tsx
+++ b/app/components/AddToCartButton.tsx
@@ -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({
{added ? "Hinzugefügt ✓" : displayLabel}
+ {currentlyOutOfStock && 0 ? (selectedVariant ?? "") : ""} />}
);
}
diff --git a/app/components/AddToCartInlineButton.tsx b/app/components/AddToCartInlineButton.tsx
index 1c5ffa5..b7dffed 100644
--- a/app/components/AddToCartInlineButton.tsx
+++ b/app/components/AddToCartInlineButton.tsx
@@ -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({
+ {currentlyOutOfStock && 0 ? (selectedVariant ?? "") : ""} />}
);
}
diff --git a/app/components/NotifyMeForm.tsx b/app/components/NotifyMeForm.tsx
new file mode 100644
index 0000000..d0db649
--- /dev/null
+++ b/app/components/NotifyMeForm.tsx
@@ -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 Danke! Wir melden uns, sobald es wieder verfügbar ist.
;
+ }
+
+ return (
+
+ );
+}
diff --git a/app/components/ProductSpotlight.tsx b/app/components/ProductSpotlight.tsx
index 9242b3b..74b0f87 100644
--- a/app/components/ProductSpotlight.tsx
+++ b/app/components/ProductSpotlight.tsx
@@ -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. */}
-
+
{product.href && (
+
);
diff --git a/app/lib/stockNotifications.ts b/app/lib/stockNotifications.ts
new file mode 100644
index 0000000..cae2c36
--- /dev/null
+++ b/app/lib/stockNotifications.ts
@@ -0,0 +1,25 @@
+// Server-only — imported exclusively by app/api/stock-notifications/route.ts.
+// Reuses ORDER_SERVICE_SECRET rather than a new dedicated secret, matching
+// StockNotifications.ts's own choice on the backend (see that collection's
+// comment) — it's already the general-purpose "frontend server calling
+// into Payload" credential used by getWishlistEnabled/getSearchEnabled/etc.
+const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de";
+const SERVICE_SECRET = process.env.ORDER_SERVICE_SECRET || "";
+
+export async function createStockNotification(email: string, productId: number, variantName: string): Promise<{ ok: boolean; reason?: string }> {
+ const res = await fetch(`${PAYLOAD_URL}/api/stock-notifications`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json", "x-order-service-secret": SERVICE_SECRET },
+ body: JSON.stringify({ email, product: productId, variantName }),
+ });
+ if (res.ok) return { ok: true };
+
+ // The (tenant, email, product, variantName) unique index rejects a
+ // second signup for the same thing with a 400 — read as "already on the
+ // list", not a real error, so the UI can still show success rather than
+ // a confusing failure for someone who's already signed up.
+ if (res.status === 400) return { ok: true };
+
+ console.error(`createStockNotification: Payload returned ${res.status} ${res.statusText}`);
+ return { ok: false, reason: "Eintragen hat nicht geklappt. Bitte versuch es später erneut." };
+}
diff --git a/app/shop/components/ProductGrid.tsx b/app/shop/components/ProductGrid.tsx
index 1e3d704..ecc4dc6 100644
--- a/app/shop/components/ProductGrid.tsx
+++ b/app/shop/components/ProductGrid.tsx
@@ -218,7 +218,7 @@ export async function ProductGrid({
equal-height lesson). */}
-
+
);
diff --git a/app/todo-cards/components/Pricing.tsx b/app/todo-cards/components/Pricing.tsx
index 758293e..e586109 100644
--- a/app/todo-cards/components/Pricing.tsx
+++ b/app/todo-cards/components/Pricing.tsx
@@ -125,6 +125,7 @@ export async function Pricing() {