Add mobile swipe gallery, shared ProductCard, dynamic Datenschutz address, GSC verification
- ProductGallery: touch swipe (left/right) now navigates slides on mobile, previously click-only. - New shared ProductCard component used by ProductGrid/RelatedProducts/ MerklisteGrid — product title is now the card's link (replaces the separate "Mehr erfahren" line), consistent aspect ratio across all three grids, more compact cards. - Datenschutz: name/address/email now come from company-settings via a new VerantwortlicherBlock, same single-source pattern as Impressum's AnbieterAngaben — no more hand-typed address to keep in sync. - layout.tsx: render googleSearchConsoleVerification via Next's native verification.google metadata field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { headingId } from "../../components/RichText";
|
||||
import type { CompanySettings } from "../../lib/payload";
|
||||
import type { TOCSection } from "../../components/SectionTOC";
|
||||
|
||||
// Renders "1. Verantwortlicher" straight from company-settings, same
|
||||
// single-source-of-truth pattern as Impressum's AnbieterAngaben.tsx — this
|
||||
// used to be hand-typed name/address/email baked into the Datenschutz
|
||||
// richText (seed-datenschutz.ts on the Payload side), silently out of
|
||||
// date the moment company-settings changed without a matching manual
|
||||
// edit here too. The heading itself stays numbered "1." (headingId's own
|
||||
// `^(\d+)\.` match turns that into "section-1", exactly the id this
|
||||
// section's TOC entry already had before the RichText stopped rendering
|
||||
// it — no anchor breakage).
|
||||
export function verantwortlicherHeadings(): TOCSection[] {
|
||||
return [{ id: headingId("1. Verantwortlicher"), title: "1. Verantwortlicher" }];
|
||||
}
|
||||
|
||||
function Heading({ children }: { children: string }) {
|
||||
return (
|
||||
<h2
|
||||
id={headingId(children)}
|
||||
className="font-semibold text-h-small text-text-primary mt-2 scroll-mt-32 first:mt-0"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
{children}
|
||||
<span className="block h-[0.125rem] w-8 bg-brand mt-2" aria-hidden />
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
function P({ children }: { children: React.ReactNode }) {
|
||||
return <p className="text-body text-text-body">{children}</p>;
|
||||
}
|
||||
|
||||
export function VerantwortlicherBlock({ seller }: { seller: CompanySettings }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<Heading>1. Verantwortlicher</Heading>
|
||||
<P>Verantwortlich für die Datenverarbeitung auf dieser Website ist:</P>
|
||||
<div className="flex flex-col gap-1">
|
||||
<P>{seller.managingDirector || seller.sellerName}</P>
|
||||
<P>{seller.sellerStreet}</P>
|
||||
<P>
|
||||
{seller.sellerZip} {seller.sellerCity}
|
||||
</P>
|
||||
<P>{seller.sellerCountry}</P>
|
||||
<P>
|
||||
E-Mail: <a href={`mailto:${seller.sellerEmail}`} className="text-brand hover:underline">{seller.sellerEmail}</a>
|
||||
</P>
|
||||
</div>
|
||||
<P>Ein gesetzlich vorgeschriebener Datenschutzbeauftragter ist für unser Unternehmen aufgrund seiner Größe derzeit nicht erforderlich.</P>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import { Footer } from "../components/Footer";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
import { LiveRichText } from "../components/LiveRichText";
|
||||
import { SectionTOC, MobileSectionTOC } from "../components/SectionTOC";
|
||||
import { getLegalPage } from "../lib/payload";
|
||||
import { getLegalPage, getCompanySettings } from "../lib/payload";
|
||||
import { formatMonthYear } from "../lib/format";
|
||||
import { VerantwortlicherBlock, verantwortlicherHeadings } from "./components/VerantwortlicherBlock";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Datenschutzerklärung",
|
||||
@@ -18,8 +19,11 @@ export const metadata: Metadata = {
|
||||
|
||||
export default async function DatenschutzPage() {
|
||||
const { isEnabled: isPreview } = await draftMode();
|
||||
const page = await getLegalPage("datenschutz", { draft: isPreview });
|
||||
const headings = page ? extractHeadings(page.content) : [];
|
||||
const [page, seller] = await Promise.all([getLegalPage("datenschutz", { draft: isPreview }), getCompanySettings()]);
|
||||
// "1. Verantwortlicher" headings first — that block renders above the
|
||||
// CMS content below (same reasoning as Impressum's own headings
|
||||
// composition, see AnbieterAngaben.tsx).
|
||||
const headings = [...verantwortlicherHeadings(), ...(page ? extractHeadings(page.content) : [])];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -69,7 +73,11 @@ export default async function DatenschutzPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:flex-1 min-w-0">
|
||||
<div className="w-full lg:flex-1 min-w-0 flex flex-col gap-8">
|
||||
{/* Name/Adresse/E-Mail kommen direkt aus company-settings, nicht
|
||||
aus der CMS-Richtext unten — single-sourced, gleiche
|
||||
Begründung wie Impressum's AnbieterAngaben.tsx. */}
|
||||
{seller && <VerantwortlicherBlock seller={seller} />}
|
||||
{page ? (
|
||||
isPreview ? <LiveRichText initialContent={page.content} /> : <RichText content={page.content} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user