Files
einfach-produktiv/app/components/AddToCartButton.tsx
T
Marco d472eb546f Move trust badges, shipping/payment methods, Werkzeuge cards, product spotlight into CMS
New Payload collections, all editable without a code deploy:
- TrustBadges: the horizontal Schneller-Versand/Versandkostenfrei/Mit-
  Liebe-verpackt row (TrustRow.tsx, now an async server component).
- CartTrustBadges: the Sichere-Zahlung/14-Tage-Rückgaberecht/Nachhaltig-
  verpackt sidebar bullets — shared by /cart (title only) and /checkout
  (title + description), which previously had two different hardcoded
  bullet lists for what's conceptually the same content.
- ShippingMethods: /checkout's Versandart radios. Each method has its own
  optional freeShippingThreshold — omitted means "never free" (Express),
  not "always free". /cart's FreeShippingBanner now targets the lowest
  threshold among active methods instead of a single global constant, and
  hides entirely if no active method has one.
- PaymentMethods: /checkout's Zahlungsart radios, icons as an array
  (Kreditkarte shows 3 logos, PayPal/Überweisung show 1).
- Products: new spotlight/spotlightHeadline/spotlightText/spotlightImage/
  compareAtPrice fields. ProductSpotlight.tsx (homepage) now shows
  whichever product has `spotlight` checked instead of being hardcoded to
  ToDo-Karten, with its own marketing copy separate from the plain
  catalog name/description. AddToCartButton takes an explicit productId
  prop now instead of a hardcoded "todo-karten" constant.
- WerkzeugeCards: the homepage's "Meine Werkzeuge" 3-card grid (Tools.tsx,
  now async). Icons use a uniform box instead of the previous per-card
  hand-tuned width/height/rotation, which only worked for 3 known,
  upside-down-authored SVGs — those were re-exported as pre-flipped PNGs.

lib/payload.ts gained getTrustBadges/getCartTrustBadges/getShippingMethods/
getPaymentMethods/getWerkzeugeCards/getSpotlightProduct, all with the same
graceful-empty-array-on-fetch-failure pattern as the existing functions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 23:15:46 +00:00

80 lines
3.3 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { addToCart } from "../lib/cart";
import { useCartFly } from "./CartFly";
const FEEDBACK_MS = 2000;
/**
* Shared by /todo-cards's Hero + pricing panel and Home's product
* spotlight. Used to navigate straight to /cart on click; now stays on the
* page instead (matching AddToCartInlineButton's behavior everywhere else)
* — adds to the cart, plays the fly-to-navbar-icon animation, and shows a
* brief inline success state instead of leaving the page.
*/
export function AddToCartButton({
label,
className,
productId = "todo-karten",
}: {
label: string;
className?: string;
/** Defaults to "todo-karten" for /todo-cards' own hardcoded usage — Home's
* ProductSpotlight passes the actual CMS-selected spotlight product's id
* explicitly, since that can now be a different product. */
productId?: string;
}) {
const [added, setAdded] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const buttonRef = useRef<HTMLButtonElement>(null);
const { fly } = useCartFly();
useEffect(() => () => clearTimeout(timeoutRef.current), []);
function handleClick() {
addToCart(productId);
if (buttonRef.current) fly(buttonRef.current);
setAdded(true);
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setAdded(false), FEEDBACK_MS);
}
const base =
className ??
"inline-flex items-center justify-center px-6 py-[0.8125rem] rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base";
// Trailing `!` — Tailwind v4's important-modifier syntax moved from a
// leading `!` to this trailing suffix — forces the success color to win
// over whatever bg-brand/text-text-primary each caller already baked
// into `base`, since appending on top can't rely on CSS source order
// the way branching a whole className (AddToCartInlineButton's
// approach) can when the base itself varies per caller.
const stateClasses = added ? "bg-success! hover:bg-success! text-white!" : "";
return (
<button
ref={buttonRef}
type="button"
onClick={handleClick}
className={`${base} ${stateClasses}`}
>
{/* CSS-grid text-stack, not just swapping the button's text node
directly — this button is inline-flex/content-sized (no w-full),
so "Hinzugefügt ✓" being shorter than most labels made the whole
button visibly shrink while showing the success state. Stacking
both possible texts in the same grid cell (both invisible ones
still contribute to sizing) reserves width for whichever is
wider, so the button's box never changes size either way. */}
<span className="relative grid">
<span className="invisible [grid-area:1/1]" aria-hidden="true">
{label}
</span>
<span className="invisible [grid-area:1/1]" aria-hidden="true">
Hinzugefügt
</span>
<span className="[grid-area:1/1]">{added ? "Hinzugefügt ✓" : label}</span>
</span>
</button>
);
}