Show product subline on cards/cart/PDP hero; brand-color trailing period

Follows Products.subline (docker/payload commit ad7156e). Cards and cart
line items showed nothing but the bare name — added the subline under it.
der-eine and todo-cards' hero taglines were hardcoded copy nearly
identical to this new field — switched both to read subline instead, so
future edits go through the CMS.

Also adds a shared ProductName component: every hand-written PDP headline
already styled a trailing "." in brand color, but the few places that
render product.name as plain text (cards, cart, the Payload-driven
tasse-die-pause hero) had silently lost that styling. Centralizes it so
it can't drift per call site again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J1Hu5bZ1kZUgKhab6yNwCt
This commit is contained in:
Marco
2026-08-26 22:26:14 +00:00
parent 21b5f41127
commit 1330e3e621
9 changed files with 63 additions and 23 deletions
+4 -2
View File
@@ -14,6 +14,7 @@ import { Reveal } from "../../components/Reveal";
import { VersandModal } from "../../components/VersandModal";
import { VatBreakdown } from "../../components/VatBreakdown";
import { ArrowLeftIcon } from "../../components/ArrowLeftIcon";
import { ProductName } from "../../components/ProductName";
import { FreeShippingBanner } from "./FreeShippingBanner";
import type { TrustBadge, ShippingSettings } from "../../lib/payload";
@@ -260,7 +261,7 @@ export function CartContent({
className="font-semibold text-h-small text-text-primary hover:text-brand transition-colors"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.name}
<ProductName name={product.name} />
{entry.variant ? ` (${entry.variant})` : ""}
</Link>
) : (
@@ -268,10 +269,11 @@ export function CartContent({
className="font-semibold text-h-small text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.name}
<ProductName name={product.name} />
{entry.variant ? ` (${entry.variant})` : ""}
</p>
)}
{product.subline && <p className="text-body-sm text-text-muted">{product.subline}</p>}
{/* Independent stacked rows, not grid siblings — no
equal-height pressure from neighboring lines, so a
plain conditional line is enough here. */}
+4 -2
View File
@@ -5,6 +5,7 @@ import { formatPrice, discountPercent } from "../lib/format";
import { effectiveTaxRate } from "../lib/cartTotals";
import { AddToCartInlineButton } from "./AddToCartInlineButton";
import { WishlistButton } from "./WishlistButton";
import { ProductName } from "./ProductName";
import type { Product } from "../lib/payload";
// Single shared card markup for every product grid (ProductGrid,
@@ -108,13 +109,14 @@ export function ProductCard({
className="font-semibold text-h4 text-text-primary w-full hover:text-brand transition-colors"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.name}
<ProductName name={product.name} />
</Link>
) : (
<p className="font-semibold text-h4 text-text-primary w-full" style={{ fontFamily: "var(--font-lora)" }}>
{product.name}
<ProductName name={product.name} />
</p>
)}
{product.subline && <p className="text-body-sm text-text-muted">{product.subline}</p>}
</div>
<p className="flex items-baseline gap-1.5">
{discount !== null && (
+16
View File
@@ -0,0 +1,16 @@
// Every hand-written PDP headline styles a trailing "." in brand color
// (e.g. der-eine/Pricing.tsx's "Der Eine<span className="text-brand">.</span>"),
// matching how every product's own Payload `name` is actually written
// ("Der Eine.", "Die Pause.", "Einfach anfangen."). Anywhere `product.name`
// is rendered as plain text instead (cards, cart line items, the one
// Payload-driven PDP hero) silently lost that styling — this centralizes
// the same trailing-period split so it can't drift per call site again.
export function ProductName({ name }: { name: string }) {
if (!name.endsWith(".")) return <>{name}</>;
return (
<>
{name.slice(0, -1)}
<span className="text-brand">.</span>
</>
);
}
+8 -6
View File
@@ -63,12 +63,14 @@ export async function Hero() {
>
Der Eine<span className="text-brand">.</span>
</p>
<p
className="font-semibold text-h3 text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Für die eine Sache, die gerade zählt.
</p>
{product?.subline && (
<p
className="font-semibold text-h3 text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.subline}
</p>
)}
</div>
<p className="text-body text-text-body">
+1
View File
@@ -6,6 +6,7 @@ const product = (overrides: Partial<Product> = {}): Product => ({
id: "todo-karten",
numericId: 1,
name: "ToDo-Karten",
subline: null,
description: null,
descriptionText: "",
sku: null,
+5
View File
@@ -181,6 +181,9 @@ export type Product = {
// slug-based call site.
numericId: number;
name: string;
// Short tagline shown under the name on cards/cart/PDP hero — see
// Products.ts's own field comment. null/"" means show nothing extra.
subline: string | null;
// Raw Lexical richText JSON — rendered formatted (bold/italic/multiple
// paragraphs) on the PDP via the shared <RichText> component. Plain-text
// consumers (Passend-dazu cards, JSON-LD) use `descriptionText` below
@@ -271,6 +274,7 @@ export type Product = {
type PayloadProduct = {
id: number;
name: string;
subline: string | null;
slug: string;
description: unknown | null;
sku: string | null;
@@ -356,6 +360,7 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
id: product.slug,
numericId: product.id,
name: product.name,
subline: product.subline || null,
description: product.description ?? null,
descriptionText: extractPlainText(descriptionRoot),
sku: product.sku || null,
+14 -6
View File
@@ -3,6 +3,7 @@ import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
import { RichText } from "../../components/RichText";
import { ProductName } from "../../components/ProductName";
import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
import { effectiveTaxRate } from "../../lib/cartTotals";
@@ -44,12 +45,19 @@ export async function Hero() {
</p>
<div className="flex flex-col gap-6 items-start w-full flex-1 lg:justify-center">
<p
className="font-semibold text-h-page text-text-primary"
style={{ fontFamily: "var(--font-playfair)" }}
>
{product?.name ?? "Tasse"}
</p>
<div className="flex flex-col gap-2 items-start w-full">
<p
className="font-semibold text-h-page text-text-primary"
style={{ fontFamily: "var(--font-playfair)" }}
>
{product ? <ProductName name={product.name} /> : "Tasse"}
</p>
{product?.subline && (
<p className="font-semibold text-h3 text-text-primary" style={{ fontFamily: "var(--font-lora)" }}>
{product.subline}
</p>
)}
</div>
{product?.description ? <RichText content={product.description} /> : null}
+3 -1
View File
@@ -2,6 +2,7 @@ import Image from "next/image";
import { AddToCartButton } from "../../components/AddToCartButton";
import { Reveal } from "../../components/Reveal";
import { RichText } from "../../components/RichText";
import { ProductName } from "../../components/ProductName";
import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
import { formatPrice, discountPercent } from "../../lib/format";
import { effectiveTaxRate } from "../../lib/cartTotals";
@@ -47,8 +48,9 @@ export async function Pricing() {
className="font-semibold text-h-small text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.name}
<ProductName name={product.name} />
</p>
{product.subline && <p className="text-body-sm font-semibold text-text-primary">{product.subline}</p>}
{product.description ? <RichText content={product.description} /> : null}
</div>
+8 -6
View File
@@ -97,12 +97,14 @@ export async function TodoKartenHero() {
>
ToDo-Karten<span className="text-brand">.</span>
</p>
<p
className="font-semibold text-h3 text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Kleine Karten. Große Wirkung.
</p>
{product?.subline && (
<p
className="font-semibold text-h3 text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
{product.subline}
</p>
)}
</div>
<p className="text-body text-text-body">