Add shop, cart, versand pages and Impulse & Tipps detail page
- New /shop overview, /cart (real cart state via useSyncExternalStore), /versand (shipping policy page with TOC) and /newsletter detail page - Cart: quantity/removal, order summary, related-products cross-sell with randomized picks, VersandModal quick-reference instead of navigating away, MwSt. disclosure next to unit prices - Add-to-cart UX: inline success feedback (green state) plus a fly-to-navbar-cart-icon animation (CartFlyProvider) with a delayed badge count-up; AddToCartButton no longer navigates straight to /cart - Shared lib/products.ts catalog and lib/shipping.ts constants (cost, free-shipping threshold, handling/transit days) so cart, trust badges and the versand page can never drift apart - Fix Navbar smooth-scroll easing (ease-out instead of ease-in-out, no more perceived start delay); compress newsletter modal photo 2.4MB -> 85KB to fix first-open jank
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import Link from "next/link";
|
||||
import {
|
||||
SHIPPING_COST,
|
||||
FREE_SHIPPING_THRESHOLD,
|
||||
HANDLING_DAYS,
|
||||
TRANSIT_DAYS_DE,
|
||||
TOTAL_DAYS_DE,
|
||||
} from "../../lib/shipping";
|
||||
import { formatPrice } from "../../lib/products";
|
||||
|
||||
// Single source of truth for both the full /versand page and the cart's
|
||||
// quick-reference VersandModal — same section ids/titles/copy either way,
|
||||
// so the two can never drift apart. `withAnchors` adds scroll-margin (for
|
||||
// the full page's TOC) — the modal doesn't need it, it just scrolls its
|
||||
// own dialog body.
|
||||
export const VERSAND_SECTION_IDS = [
|
||||
{ id: "lieferzeiten", title: "Lieferzeiten" },
|
||||
{ id: "versandkosten", title: "Versandkosten" },
|
||||
{ id: "liefergebiet", title: "Liefergebiet" },
|
||||
{ id: "versanddienstleister", title: "Versanddienstleister" },
|
||||
{ id: "transportrisiko", title: "Transportrisiko" },
|
||||
{ id: "widerruf", title: "Rückgabe & Widerruf" },
|
||||
] as const;
|
||||
|
||||
function Section({
|
||||
id,
|
||||
title,
|
||||
withAnchor,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
withAnchor: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className={"flex flex-col gap-3 items-start w-full" + (withAnchor ? " scroll-mt-32" : "")}
|
||||
>
|
||||
<p
|
||||
className="font-semibold text-h-small text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex flex-col gap-2 items-start text-body text-text-body w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function VersandSections({ withAnchors = false }: { withAnchors?: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 items-start w-full">
|
||||
<Section id="lieferzeiten" title="Lieferzeiten" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Nach Zahlungseingang bereiten wir deine Bestellung in der Regel innerhalb von{" "}
|
||||
{HANDLING_DAYS.min}–{HANDLING_DAYS.max} Werktagen zum Versand vor. Die Versanddauer
|
||||
innerhalb Deutschlands beträgt anschließend zusätzlich {TRANSIT_DAYS_DE.min}–
|
||||
{TRANSIT_DAYS_DE.max} Werktage.
|
||||
</p>
|
||||
<p>
|
||||
Damit ist deine Bestellung in der Regel nach {TOTAL_DAYS_DE.min}–{TOTAL_DAYS_DE.max}{" "}
|
||||
Werktagen bei dir, sofern auf der jeweiligen Produktseite nichts anderes angegeben ist.
|
||||
Als Werktage gelten Montag bis Freitag, ausgenommen gesetzliche Feiertage.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section id="versandkosten" title="Versandkosten" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Die Versandkosten innerhalb Deutschlands betragen pauschal {formatPrice(SHIPPING_COST)}{" "}
|
||||
pro Bestellung. Ab einem Bestellwert von {formatPrice(FREE_SHIPPING_THRESHOLD)} versenden
|
||||
wir kostenlos.
|
||||
</p>
|
||||
<p>Alle angegebenen Preise verstehen sich inklusive der gesetzlichen Mehrwertsteuer.</p>
|
||||
</Section>
|
||||
|
||||
<Section id="liefergebiet" title="Liefergebiet" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Aktuell versenden wir ausschließlich innerhalb Deutschlands. Eine Lieferung ins Ausland
|
||||
ist derzeit nicht möglich.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section id="versanddienstleister" title="Versanddienstleister" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Der Versand erfolgt über DHL. Sobald deine Bestellung das Lager verlässt, erhältst du
|
||||
eine E-Mail mit der Sendungsverfolgung.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section id="transportrisiko" title="Transportrisiko" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Bestellst du als Verbraucher:in, trägt das Transportrisiko bei Beschädigung oder Verlust
|
||||
während des Versands grundsätzlich einfach produktiv — nicht du.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
<Section id="widerruf" title="Rückgabe & Widerruf" withAnchor={withAnchors}>
|
||||
<p>
|
||||
Informationen zu Rückgabe, Rücksendekosten und deinem gesetzlichen Widerrufsrecht findest
|
||||
du in unserer{" "}
|
||||
<Link href="/widerruf" className="text-brand hover:underline">
|
||||
Widerrufsbelehrung
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { VERSAND_SECTION_IDS } from "./VersandSections";
|
||||
|
||||
// lg:-only sidebar — same "wide fixed-width block next to content" shape
|
||||
// as the cart's order-summary sidebar (see figma-to-nextjs skill Gotcha
|
||||
// #5): a 360px TOC card plus a readable content column already exceeds
|
||||
// the 768px Tablet floor, so md: wouldn't leave room for a real 2-column
|
||||
// split at Tablet widths.
|
||||
export function VersandTOC() {
|
||||
const [active, setActive] = useState<string>(VERSAND_SECTION_IDS[0].id);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries.filter((entry) => entry.isIntersecting);
|
||||
if (visible.length > 0) setActive(visible[0].target.id);
|
||||
},
|
||||
{ rootMargin: "-120px 0px -65% 0px", threshold: 0 }
|
||||
);
|
||||
VERSAND_SECTION_IDS.forEach(({ id }) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) observer.observe(el);
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className="hidden lg:flex flex-col gap-1 w-[22.5rem] shrink-0 bg-bg-base border border-border rounded-md p-6 sticky top-32 self-start">
|
||||
<p className="text-label font-semibold text-text-muted uppercase tracking-wide mb-2">
|
||||
Inhaltsverzeichnis
|
||||
</p>
|
||||
{VERSAND_SECTION_IDS.map(({ id, title }) => (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={
|
||||
"px-3 py-2 rounded-sm text-body-sm transition-colors border-l-2 " +
|
||||
(active === id
|
||||
? "border-toc-active-border bg-bg-muted text-text-primary font-semibold"
|
||||
: "border-transparent text-text-muted hover:text-text-primary")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { VersandSections } from "./components/VersandSections";
|
||||
import { VersandTOC } from "./components/VersandTOC";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Versand",
|
||||
description:
|
||||
"Versandkosten, Lieferzeiten und Liefergebiet für Bestellungen bei einfach produktiv.",
|
||||
alternates: { canonical: "/versand" },
|
||||
};
|
||||
|
||||
export default function VersandPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1 bg-bg-base">
|
||||
<Reveal className="flex flex-col gap-3 items-start pb-6 pt-10 px-[var(--layout-padding-x)] w-full">
|
||||
<p className="flex items-center gap-2 text-body-sm text-text-muted">
|
||||
<Link href="/" className="hover:text-brand transition-colors">Startseite</Link>
|
||||
<span>›</span>
|
||||
<span className="text-text-primary">Versand</span>
|
||||
</p>
|
||||
<p
|
||||
className="font-semibold text-h-feature text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Versand
|
||||
</p>
|
||||
<p className="text-body text-text-muted">
|
||||
Transparent, fair und pünktlich — alles zu Kosten, Lieferzeiten und Liefergebiet.
|
||||
</p>
|
||||
</Reveal>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 items-start pb-16 pt-2 px-[var(--layout-padding-x)] w-full">
|
||||
<VersandTOC />
|
||||
<div className="w-full lg:flex-1 max-w-[45rem]">
|
||||
<VersandSections withAnchors />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user