Der Eine.: add Testimonials + closing Pricing CTA, reorder PasstDazu last
Testimonials now pulled from Payload (page: "der-eine", same collection as todo-cards/newsletter/challenge) instead of a hardcoded array. Pricing adds a second, final buy CTA (same image+price+button card as the other product pages' Pricing.tsx) for visitors who scroll past Hero's button. PasstDazu moves to right before the Footer.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import Image from "next/image";
|
||||
import { AddToCartButton } from "../../components/AddToCartButton";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { getProductBySlug, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../../lib/payload";
|
||||
import { formatPrice, discountPercent } from "../../lib/format";
|
||||
import { effectiveTaxRate } from "../../lib/cartTotals";
|
||||
|
||||
// Closing buy banner — same image+price+button card as todo-cards' and
|
||||
// tasse-die-pause's own Pricing.tsx, repeated here as a second, final
|
||||
// call-to-action after Testimonials so a visitor who scrolled past the
|
||||
// Hero's buy button without ordering gets another prompt right before the
|
||||
// footer.
|
||||
export async function Pricing() {
|
||||
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
|
||||
getProductBySlug("stift-kugelschreiber"),
|
||||
getShippingSettings(),
|
||||
getDefaultTaxRatePercent(),
|
||||
getKleinunternehmer(),
|
||||
]);
|
||||
if (!product) return null;
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
const taxRate = effectiveTaxRate(product, defaultTaxRate);
|
||||
const fullyOutOfStock =
|
||||
!product.active || (product.variants.length > 0 ? product.variants.every((v) => v.outOfStock) : product.outOfStock);
|
||||
const anyLowStock = product.active && (product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock);
|
||||
|
||||
return (
|
||||
<section className="w-full bg-bg-base px-[var(--layout-padding-x)] py-8">
|
||||
<Reveal className="bg-bg-muted rounded-md flex flex-col lg:flex-row gap-8 lg:gap-12 items-center p-6 lg:pl-8 lg:pr-10 lg:py-6">
|
||||
<div className="relative w-full lg:w-[25.625rem] lg:shrink-0 aspect-[410/227] rounded-sm overflow-hidden">
|
||||
<Image src={product.image} alt={product.name} fill sizes="(min-width: 1024px) 410px, 100vw" className="object-cover" />
|
||||
{fullyOutOfStock ? (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-text-muted px-2.5 py-1 text-label font-bold text-bg-base">
|
||||
Ausverkauft
|
||||
</span>
|
||||
) : (
|
||||
discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 items-start flex-1 min-w-0 w-full">
|
||||
<p
|
||||
className="font-semibold text-h-small text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Der Eine<span className="text-brand">.</span> jetzt sichern
|
||||
</p>
|
||||
<p className="text-body-sm text-text-primary">
|
||||
Ein guter Stift muss nicht kompliziert sein. Hol dir Der Eine. und hab immer etwas zur Hand, wenn dir gerade etwas durch den Kopf geht.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 items-start w-full lg:w-[18.75rem] lg:shrink-0">
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<div className="flex gap-2 items-baseline">
|
||||
{discount !== null && (
|
||||
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
|
||||
)}
|
||||
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
|
||||
</div>
|
||||
<p className="text-label text-text-muted">
|
||||
{kleinunternehmer
|
||||
? product.noShippingCost
|
||||
? "Keine Versandkosten"
|
||||
: "zzgl. Versand"
|
||||
: `inkl. ${taxRate}% MwSt. ${product.noShippingCost ? "– keine Versandkosten" : "zzgl. Versand"}`}
|
||||
</p>
|
||||
{!product.noShippingCost && (
|
||||
<p className="text-label text-text-muted">
|
||||
Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
|
||||
<AddToCartButton
|
||||
label="Der Eine. bestellen"
|
||||
className="w-full 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 text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-muted"
|
||||
productId={product.id}
|
||||
numericId={product.numericId}
|
||||
outOfStock={!product.active || product.outOfStock}
|
||||
maxQty={product.maxQty}
|
||||
variants={product.active ? product.variants : []}
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { draftMode } from "next/headers";
|
||||
import { TestimonialsGrid } from "../../components/TestimonialsGrid";
|
||||
import { LiveTestimonialsGrid } from "../../components/LiveTestimonialsGrid";
|
||||
import { getTestimonials } from "../../lib/payload";
|
||||
|
||||
// Payload-driven like todo-cards/newsletter/challenge's own testimonial
|
||||
// grids (see Testimonials collection's "page" field) — no longer a
|
||||
// hardcoded array now that "der-eine" is a valid page value there.
|
||||
export async function Testimonials() {
|
||||
const { isEnabled: isPreview } = await draftMode();
|
||||
const testimonials = await getTestimonials("der-eine", { draft: isPreview });
|
||||
|
||||
return isPreview ? (
|
||||
<LiveTestimonialsGrid testimonials={testimonials} />
|
||||
) : (
|
||||
<TestimonialsGrid testimonials={testimonials} />
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
||||
import { Hero } from "./components/Hero";
|
||||
import { Focus } from "./components/Focus";
|
||||
import { PasstDazu } from "./components/PasstDazu";
|
||||
import { Testimonials } from "./components/Testimonials";
|
||||
import { Pricing } from "./components/Pricing";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { getProductBySlug, getCompanySettings } from "../lib/payload";
|
||||
import { buildProductSchema } from "../lib/structuredData";
|
||||
@@ -44,6 +46,8 @@ export default async function DerEinePage() {
|
||||
<main className="flex flex-col flex-1">
|
||||
<Hero />
|
||||
<Focus />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
<PasstDazu productSlug="stift-kugelschreiber" />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
+5
-5
@@ -817,7 +817,7 @@ export async function getWerkzeugeCards(): Promise<WerkzeugeCard[]> {
|
||||
}));
|
||||
}
|
||||
|
||||
export type TestimonialsPage = "todo-cards" | "newsletter" | "challenge";
|
||||
export type TestimonialsPage = "todo-cards" | "newsletter" | "challenge" | "der-eine";
|
||||
|
||||
export type Testimonial = { id: number; quote: string; name: string; role: string; avatar: string };
|
||||
|
||||
@@ -842,10 +842,10 @@ export function mapPayloadTestimonial(doc: PayloadTestimonial): Testimonial {
|
||||
}
|
||||
|
||||
// Powers the identically-styled customer testimonial grids on /todo-cards,
|
||||
// /newsletter, and /challenge (see TestimonialsGrid.tsx) — previously 3
|
||||
// hardcoded arrays kept manually in sync. The single-quote "photo band"
|
||||
// testimonials on /not-found and /bestellbestaetigung are a different
|
||||
// shape (no avatar/role) and are not part of this collection.
|
||||
// /newsletter, /challenge, and /der-eine (see TestimonialsGrid.tsx) —
|
||||
// previously hardcoded arrays kept manually in sync. The single-quote
|
||||
// "photo band" testimonials on /not-found and /bestellbestaetigung are a
|
||||
// different shape (no avatar/role) and are not part of this collection.
|
||||
export async function getTestimonials(page: TestimonialsPage, options?: { draft?: boolean }): Promise<Testimonial[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
|
||||
Reference in New Issue
Block a user