Compare commits
4 Commits
8273989d01
...
af00fd091f
| Author | SHA1 | Date | |
|---|---|---|---|
| af00fd091f | |||
| a560ec9434 | |||
| cc9da6ac6d | |||
| a72aea1070 |
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion, useInView } from "motion/react";
|
||||
import { formatPrice } from "../../lib/format";
|
||||
|
||||
const SUCCESS_VISIBLE_MS = 2500;
|
||||
@@ -53,14 +53,29 @@ function FreeShippingBannerInner({ subtotal, threshold }: { subtotal: number; th
|
||||
}
|
||||
}
|
||||
|
||||
// Gates the hide-timer on the banner being CURRENTLY visible — deliberately
|
||||
// not `{ once: true }`: since the banner sits at the very top of the cart
|
||||
// page, it's already visible the instant the page loads (well before the
|
||||
// user ever scrolls anywhere), so a lifetime "has this ever been seen"
|
||||
// flag flips true immediately and defeats the whole point — reaching the
|
||||
// threshold later while scrolled away would still start the timer right
|
||||
// then, exactly the bug this was meant to fix. Continuous tracking
|
||||
// instead: the effect below only runs the countdown while `isInView` is
|
||||
// true, and its own cleanup cancels it the moment the banner scrolls back
|
||||
// out of view — so it always takes a full uninterrupted 2.5s of the
|
||||
// banner actually being on screen before it's allowed to hide, restarting
|
||||
// if the user looks away mid-countdown and comes back.
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref);
|
||||
|
||||
// Effect's own cleanup (not a ref) cancels the pending hide-timer
|
||||
// whenever phase changes away from "success" (e.g. dropping back below
|
||||
// the threshold before the timer fires) or on unmount.
|
||||
// whenever phase changes away from "success", the banner scrolls out of
|
||||
// view, or on unmount.
|
||||
useEffect(() => {
|
||||
if (phase !== "success") return;
|
||||
if (phase !== "success" || !isInView) return;
|
||||
const t = setTimeout(() => setPhase("hidden"), SUCCESS_VISIBLE_MS);
|
||||
return () => clearTimeout(t);
|
||||
}, [phase]);
|
||||
}, [phase, isInView]);
|
||||
|
||||
const remaining = Math.max(0, threshold - subtotal);
|
||||
const progressPct = Math.min(100, (subtotal / threshold) * 100);
|
||||
@@ -73,6 +88,7 @@ function FreeShippingBannerInner({ subtotal, threshold }: { subtotal: number; th
|
||||
<AnimatePresence>
|
||||
{phase !== "hidden" && (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={false}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
|
||||
+44
-12
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../components/Reveal";
|
||||
|
||||
@@ -154,6 +155,28 @@ function EmailCapture({ buttonLabel = "Challenge starten" }: { buttonLabel?: str
|
||||
{buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
{/* Consent checkbox — this signup's legal basis is consent (email
|
||||
marketing), same wording as the other newsletter forms; colors
|
||||
match this page's own hardcoded palette instead of the shared
|
||||
design tokens, consistent with the rest of the page. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-[#d9d9d9] accent-[#f6a701]"
|
||||
/>
|
||||
<span className="text-[0.8rem] text-[#444] leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-[#f6a701]"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
<p className="flex items-center gap-1.5 text-[0.8rem] text-[#888]">
|
||||
<LockIcon />
|
||||
Keine Werbung. Jederzeit abbestellbar.
|
||||
@@ -241,10 +264,12 @@ export default function ChallengePage() {
|
||||
todo-cards hero images; delay={0.15} mirrors their text→image
|
||||
stagger. */}
|
||||
<Reveal className="hidden lg:block group flex-1 relative overflow-hidden" delay={0.15}>
|
||||
<img
|
||||
<Image
|
||||
alt="7-Tage-Challenge"
|
||||
src="/blog-featured.jpg"
|
||||
className="absolute inset-0 w-full h-full object-cover object-center pointer-events-none transition-transform duration-500 group-hover:scale-105"
|
||||
fill
|
||||
sizes="48vw"
|
||||
className="object-cover object-center pointer-events-none transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
{/* Badge */}
|
||||
<div className="absolute top-8 right-8 w-[7.5rem] h-[7.5rem] rounded-full bg-white shadow-md flex flex-col items-center justify-center text-center p-3 gap-1">
|
||||
@@ -293,9 +318,11 @@ export default function ChallengePage() {
|
||||
// stacked below lg, this page's own structural
|
||||
// breakpoint) rather than hidden below lg like before.
|
||||
<div key={`arrow-${i}`} className="flex items-center justify-center shrink-0 lg:mt-5">
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 rotate-90 lg:w-10 lg:h-3 lg:rotate-0"
|
||||
/>
|
||||
</div>
|
||||
@@ -324,14 +351,15 @@ export default function ChallengePage() {
|
||||
|
||||
{/* Image */}
|
||||
<Reveal
|
||||
className="group w-full lg:w-[44%] lg:shrink-0 rounded-xl overflow-hidden"
|
||||
className="group relative w-full lg:w-[44%] lg:shrink-0 rounded-xl overflow-hidden"
|
||||
style={{ minHeight: "18rem" }}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
alt="Notizbuch"
|
||||
src="/challenge-content.jpg"
|
||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={{ minHeight: "18rem" }}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 44vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
</Reveal>
|
||||
|
||||
@@ -389,11 +417,15 @@ export default function ChallengePage() {
|
||||
</span>
|
||||
<p className="text-[#222221] text-[0.95rem] leading-[1.6] flex-1 pr-8">{t.quote}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
alt={t.name}
|
||||
src={t.avatar}
|
||||
className="w-10 h-10 rounded-full object-cover shrink-0 transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<div className="relative size-10 shrink-0 rounded-full overflow-hidden">
|
||||
<Image
|
||||
alt={t.name}
|
||||
src={t.avatar}
|
||||
fill
|
||||
sizes="40px"
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-[#222221] text-[0.9rem]">{t.name}</p>
|
||||
<p className="text-[#777] text-[0.8rem]">{t.role}</p>
|
||||
|
||||
@@ -293,7 +293,9 @@ export function CheckoutContent({
|
||||
<span className="flex-1 text-body-sm text-text-primary">{method.title}</span>
|
||||
<span className="flex items-center gap-2 shrink-0">
|
||||
{method.icons.map((icon, i) => (
|
||||
<img key={i} alt="" src={icon} className="h-5 w-auto" />
|
||||
<div key={i} className="relative h-5 w-8 shrink-0">
|
||||
<Image src={icon} alt="" fill sizes="32px" className="object-contain" />
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
</label>
|
||||
@@ -307,8 +309,19 @@ export function CheckoutContent({
|
||||
Jetzt kaufen (zahlungspflichtig)
|
||||
</Link>
|
||||
|
||||
<p className="text-label text-text-muted text-center w-full">
|
||||
Mit dem Kauf akzeptierst du unsere{" "}
|
||||
<Link href="/agb" target="_blank" rel="noopener noreferrer" className="underline hover:text-brand">
|
||||
AGB
|
||||
</Link>{" "}
|
||||
und die{" "}
|
||||
<Link href="/datenschutz" target="_blank" rel="noopener noreferrer" className="underline hover:text-brand">
|
||||
Datenschutzerklärung
|
||||
</Link>.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 items-center justify-center w-full">
|
||||
<img alt="" src="/icon-lock.svg" className="w-3.5 h-4" />
|
||||
<Image alt="" src="/icon-lock.svg" width={14} height={16} className="w-3.5 h-4" />
|
||||
<span className="text-body-sm text-text-muted">Sichere SSL-Verschlüsselung</span>
|
||||
</div>
|
||||
</Reveal>
|
||||
@@ -398,7 +411,7 @@ export function CheckoutContent({
|
||||
<div className="flex flex-col gap-4 items-start w-full">
|
||||
{trustBadges.map((b) => (
|
||||
<div key={b.id} className="flex gap-3 items-start w-full">
|
||||
<img alt="" src={b.icon} className="size-5 shrink-0 object-contain mt-0.5" />
|
||||
<Image alt="" src={b.icon} width={20} height={20} className="size-5 shrink-0 object-contain mt-0.5" />
|
||||
<div className="flex-1 flex flex-col gap-0.5">
|
||||
<p className="text-body-sm font-semibold text-text-primary">{b.title}</p>
|
||||
<p className="text-label text-text-muted">{b.description}</p>
|
||||
@@ -412,7 +425,7 @@ export function CheckoutContent({
|
||||
which puts the divider under the icon — the two aren't meant
|
||||
to be identical, see project notes on this pattern). */}
|
||||
<div className="bg-bg-muted flex gap-5 items-start p-6 rounded-md w-full">
|
||||
<img alt="" src="/icon-envelope-hint.png" className="h-8 w-10 shrink-0 object-contain" />
|
||||
<Image alt="" src="/icon-envelope-hint.png" width={40} height={32} className="h-8 w-10 shrink-0 object-contain" />
|
||||
<div className="flex flex-col gap-2 items-start flex-1 min-w-0 text-text-primary">
|
||||
{/* Conditional on the (unchecked-by-default) consent
|
||||
checkbox above, not stated as a given — a pre-ticked
|
||||
|
||||
@@ -170,10 +170,25 @@ export function Navbar() {
|
||||
// whenever pathname becomes "/" (covers both the initial load and a
|
||||
// client-side transition landing here), a short delay lets layout
|
||||
// settle first.
|
||||
//
|
||||
// The no-hash branch below matters just as much: since the Navbar lives
|
||||
// in the root layout (never unmounts across navigations), Next.js's
|
||||
// default Link scroll behavior treats "/" as already-visible and leaves
|
||||
// the current scrollY untouched instead of resetting to top (see
|
||||
// next/dist/docs .../link.md's "maintain scroll position" default).
|
||||
// Landing on Home from a page scrolled halfway down (e.g. clicking the
|
||||
// logo from a scrolled /shop) then visually "lands" wherever that old
|
||||
// offset happens to fall in Home's layout — often right around the
|
||||
// Werkzeuge section — instead of at the top. Forcing scrollTo(0, 0) here
|
||||
// makes a plain logo/Home navigation always start at the top, exactly
|
||||
// like the same-page click handler below already does.
|
||||
useEffect(() => {
|
||||
if (pathname !== "/") return;
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (!anchorIds.includes(hash)) return;
|
||||
if (!anchorIds.includes(hash)) {
|
||||
window.scrollTo(0, 0);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
const el = document.getElementById(hash);
|
||||
if (el) {
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
// Same lock icon + copy as /challenge's and /newsletter's EmailCapture —
|
||||
// unified across all newsletter-signup forms instead of each having its
|
||||
// own wording/color for this trust note.
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="shrink-0">
|
||||
<rect x="2" y="6" width="10" height="7" rx="1.5" stroke="#888" strokeWidth="1.3" />
|
||||
<path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="#888" strokeWidth="1.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type NewsletterProps = {
|
||||
title?: ReactNode;
|
||||
description?: string;
|
||||
@@ -32,9 +46,11 @@ export function Newsletter({
|
||||
{/* Decorative envelope icon, tilted -4° as per design */}
|
||||
<div className="flex items-center justify-center shrink-0 w-[4.23rem] h-[3.71rem]">
|
||||
<div className="-rotate-4 -scale-y-100">
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/newsletter-icon.svg"
|
||||
width={64}
|
||||
height={55}
|
||||
className="w-16 h-[3.438rem] block"
|
||||
/>
|
||||
</div>
|
||||
@@ -73,9 +89,34 @@ export function Newsletter({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Privacy note */}
|
||||
<p className="text-label text-text-primary font-normal leading-normal">
|
||||
Ich achte auf deine Daten. Kein Spam, jederzeit abbestellbar.
|
||||
{/* Consent checkbox — required since this signup's legal
|
||||
basis is consent (email marketing), not the "Ich achte
|
||||
auf deine Daten" trust note alone. Same wording/pattern
|
||||
as NewsletterModal's checkbox. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
|
||||
/>
|
||||
<span className="text-label text-text-primary font-normal leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-brand"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Privacy note — same icon/copy/color as the other
|
||||
newsletter forms (see /challenge's EmailCapture). */}
|
||||
<p className="flex items-center gap-1.5 text-label text-[#888] font-normal leading-normal">
|
||||
<LockIcon />
|
||||
Keine Werbung. Jederzeit abbestellbar.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
const features = [
|
||||
@@ -149,7 +150,7 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
|
||||
aria-label="Schließen"
|
||||
className="absolute top-6 right-6 z-10 size-6 flex items-center justify-center active:scale-90 transition-transform"
|
||||
>
|
||||
<img alt="" src="/icon-close.png" className="size-full object-contain" />
|
||||
<Image alt="" src="/icon-close.png" width={24} height={24} className="size-full object-contain" />
|
||||
</button>
|
||||
|
||||
{/* modal-top: photo + copy/form, stacked below md */}
|
||||
@@ -170,7 +171,7 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
|
||||
uses this exact same asset); without it the icon renders
|
||||
flipped. */}
|
||||
<div className="w-16 h-14 -rotate-4 -scale-y-100">
|
||||
<img alt="" src="/newsletter-icon.svg" className="w-full h-full" />
|
||||
<Image alt="" src="/newsletter-icon.svg" width={64} height={56} className="w-full h-full" />
|
||||
</div>
|
||||
|
||||
<p
|
||||
@@ -205,7 +206,16 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
|
||||
className="size-4 shrink-0 rounded-xs border border-border accent-brand"
|
||||
/>
|
||||
<span className="text-label text-text-primary">
|
||||
Ich akzeptiere die Datenschutzerklärung.
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-brand"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
@@ -224,8 +234,8 @@ export function NewsletterModal({ open, onClose }: { open: boolean; onClose: ()
|
||||
<div className="flex flex-col md:flex-row items-start px-8 md:px-20 py-6 md:py-9 gap-8 md:gap-6">
|
||||
{features.map((f) => (
|
||||
<div key={f.title} className="flex-1 flex gap-6 items-start w-full">
|
||||
<div className="h-10 w-10 shrink-0 flex items-center justify-center">
|
||||
<img alt="" src={f.icon} className="max-h-10 max-w-10 object-contain" />
|
||||
<div className="relative h-10 w-10 shrink-0 flex items-center justify-center">
|
||||
<Image alt="" src={f.icon} fill sizes="40px" className="object-contain" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 items-start flex-1 min-w-0">
|
||||
<p
|
||||
|
||||
@@ -2,6 +2,17 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
|
||||
// Same lock icon + copy as /challenge's and the shared Newsletter
|
||||
// component's trust note — unified across all newsletter-signup forms.
|
||||
function LockIcon() {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" className="shrink-0">
|
||||
<rect x="2" y="6" width="10" height="7" rx="1.5" stroke="#888" strokeWidth="1.3" />
|
||||
<path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" stroke="#888" strokeWidth="1.3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const checklist = [
|
||||
"Jeden Mittwoch neue Impulse & Tipps",
|
||||
"Kurz & knackig – in 5 Minuten gelesen",
|
||||
@@ -47,7 +58,7 @@ export function WeeklyImpulsesHero() {
|
||||
<div className="flex flex-col gap-6 items-start w-full flex-1 lg:justify-center">
|
||||
{/* Pill badge */}
|
||||
<div className="flex gap-2 items-center border border-border rounded-full pl-3.5 pr-4 py-2">
|
||||
<img alt="" src="/icon-envelope-small.svg" className="w-4 h-3 shrink-0" />
|
||||
<Image alt="" src="/icon-envelope-small.svg" width={16} height={12} className="w-4 h-3 shrink-0" />
|
||||
<span className="text-body-sm font-semibold text-text-primary whitespace-nowrap">
|
||||
Jeden Mittwoch in deinem Postfach
|
||||
</span>
|
||||
@@ -73,7 +84,7 @@ export function WeeklyImpulsesHero() {
|
||||
<ul className="flex flex-col gap-3 items-start w-full">
|
||||
{checklist.map((item) => (
|
||||
<li key={item} className="flex gap-[0.625rem] items-center w-full">
|
||||
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
|
||||
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0" />
|
||||
<span className="flex-1 text-body text-text-primary">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -96,9 +107,31 @@ export function WeeklyImpulsesHero() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Consent checkbox — this signup's legal basis is consent
|
||||
(email marketing), same wording/pattern as the shared
|
||||
Newsletter component's and NewsletterModal's checkbox. */}
|
||||
<label className="flex gap-2 items-start cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="size-4 shrink-0 mt-0.5 rounded-xs border border-border accent-brand"
|
||||
/>
|
||||
<span className="text-label text-text-primary font-normal leading-normal">
|
||||
Ich akzeptiere die{" "}
|
||||
<Link
|
||||
href="/datenschutz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-brand"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex gap-[0.375rem] items-center">
|
||||
<img alt="" src="/icon-lock.svg" className="size-3.5" />
|
||||
<span className="text-label text-text-muted">Keine Werbung. Jederzeit abbestellbar.</span>
|
||||
<LockIcon />
|
||||
<span className="text-label text-[#888]">Keine Werbung. Jederzeit abbestellbar.</span>
|
||||
</div>
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function ProductGrid() {
|
||||
{product.href && (
|
||||
<Link
|
||||
href={product.href}
|
||||
className="flex items-center gap-1 font-bold text-label text-brand hover:underline"
|
||||
className="flex items-center gap-1 font-bold text-label text-text-primary hover:text-brand transition-colors"
|
||||
>
|
||||
<span>Mehr erfahren</span>
|
||||
<span aria-hidden>→</span>
|
||||
|
||||
@@ -102,7 +102,7 @@ export function VersandSections({ withAnchors = false }: { withAnchors?: boolean
|
||||
<p>
|
||||
Informationen zu Rückgabe, Rücksendekosten und deinem gesetzlichen Widerrufsrecht findest
|
||||
du in unserer{" "}
|
||||
<Link href="/widerruf" className="text-brand hover:underline">
|
||||
<Link href="/widerruf" target="_blank" rel="noopener noreferrer" className="text-brand hover:underline">
|
||||
Widerrufsbelehrung
|
||||
</Link>
|
||||
.
|
||||
|
||||
Reference in New Issue
Block a user