Add native cookie consent and move hero intro above CTA

- Restore CookieConsent banner behavior with the new dependency
- Reposition the right-panel intro above the Projekt anfragen button
- Add focused tests and update build metadata
This commit is contained in:
2026-05-06 21:22:12 +02:00
parent ed74fd0652
commit d2ba994fad
8 changed files with 174 additions and 4 deletions

View File

@@ -0,0 +1,65 @@
---
id: TASK-2
title: Animate cookie banner transitions
status: In Progress
assignee: []
created_date: '2026-05-06 18:52'
updated_date: '2026-05-06 19:10'
labels:
- frontend
- animation
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Animate the privacy notice transition between the vanilla-cookieconsent banner and the floating cookie button in both directions using the existing Motion dependency, while preserving consent state and accessibility behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 The banner shrinks animatedly into the round cookie button.
- [ ] #2 The round cookie button opens animatedly back into the banner.
- [x] #3 prefers-reduced-motion receives a reduced fade-only variant.
- [x] #4 The production build completes successfully.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect existing cookie animation and test setup.
2. Add a focused failing test for the exported animation helpers/guards where practical.
3. Implement Motion-based bidirectional banner/FAB transitions with reduced-motion fallback.
4. Run focused verification and production build.
5. Check acceptance criteria and report for manual testing.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented Motion-based bidirectional transition in src/lib/cookie-banner-info.ts using a ghost element, transform/opacity animation, and a reduced-motion fade path.
Verified focused cookie animation test passes with node --test tests/cookie-banner-info.test.mjs.
Verified production build passes with pnpm build.
Full node --test tests/*.mjs currently has an unrelated existing failure in tests/landing-content.test.mjs because the current landing content no longer contains the expected phrase "Projektbrief".
Follow-up bugfix: native CookieConsent wrapper could remain briefly visible during the custom open animation, so the temporary ghost and the native modal appeared together. Fixed by suppressing both #cc-main .cm-wrapper and #cc-main .cm during transitions.
Follow-up bugfix: the accept button listener used a one-shot binding, so after reopening the same modal could close with CookieConsent's native fade instead of the custom banner-to-FAB transition. Fixed by binding accept buttons through a WeakSet without once:true and rebinding after show(true).
Verified follow-up with node --test tests/cookie-banner-info.test.mjs and pnpm build.
Follow-up bugfix after manual report: clicking Verstanden could still leave no visible FAB if the Motion close sequence failed or did not resolve. Removed risky borderRadius/padding from the close Motion group, forced the ghost into layout before native modal suppression, and added an idempotent finalizeBannerToFab fallback timer so the FAB is always restored.
Verified with node --test tests/cookie-banner-info.test.mjs and pnpm build.
Rolled back the custom cookie circle and Motion transition work to the stable native CookieConsent behavior requested by the user: banner appears initially and clicking Verstanden lets CookieConsent close it normally. Removed custom FAB, ghost, Motion import, and event interception from src/lib/cookie-banner-info.ts.
Updated the focused cookie test to assert the native CookieConsent close behavior and absence of the custom FAB/Motion code.
Verified rollback with node --test tests/cookie-banner-info.test.mjs and pnpm build.
<!-- SECTION:NOTES:END -->

View File

@@ -0,0 +1,40 @@
---
id: TASK-3
title: Move right-panel intro above inquiry button
status: In Progress
assignee: []
created_date: '2026-05-06 19:20'
updated_date: '2026-05-06 19:21'
labels: []
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Reposition the red-panel 01 headline and supporting copy so it sits directly above the Projekt anfragen button.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The 01 headline and supporting copy appear above the Projekt anfragen button on the right panel
- [x] #2 Responsive layout remains intact without overlapping content
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Locate the right-panel intro markup and layout rules
2. Reposition the intro block above the CTA while preserving responsive spacing
3. Run focused checks and report verification
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Moved the right-panel intro block out of vertical centering and placed it directly above the project inquiry CTA. Removed the CTA mt-auto so it follows the intro block naturally.
Verification: pnpm build passed.
Additional verification: node --test tests/landing-content.test.mjs tests/cookie-banner-info.test.mjs fails because tests/landing-content.test.mjs still expects the removed/renamed phrase Projektbrief. This appears unrelated to the layout-only change; pnpm build remains passing.
<!-- SECTION:NOTES:END -->

View File

@@ -30,6 +30,7 @@
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.3",
"tw-animate-css": "^1.4.0",
"vanilla-cookieconsent": "^3.1.0",
"zod": "^4.3.6"
},
"devDependencies": {

8
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
vanilla-cookieconsent:
specifier: ^3.1.0
version: 3.1.0
zod:
specifier: ^4.3.6
version: 4.3.6
@@ -3709,6 +3712,9 @@ packages:
resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==}
engines: {node: ^20.17.0 || >=22.9.0}
vanilla-cookieconsent@3.1.0:
resolution: {integrity: sha512-/McNRtm/3IXzb9dhqMIcbquoU45SzbN2VB+To4jxEPqMmp7uVniP6BhGLjU8MC7ZCDsNQVOp27fhQTM/ruIXAA==}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -7794,6 +7800,8 @@ snapshots:
validate-npm-package-name@7.0.2: {}
vanilla-cookieconsent@3.1.0: {}
vary@1.1.2: {}
vfile-location@5.0.3:

View File

@@ -117,8 +117,8 @@ const LandingHeroSection = () => {
</button>
</div>
<div className="relative flex min-h-0 flex-1 flex-col justify-center py-10 lg:py-14">
<div className="pointer-events-none absolute bottom-8 right-0 h-36 w-36 border border-primary-foreground/35 sm:bottom-10 sm:right-2 lg:bottom-16 lg:right-10" />
<div className="relative mt-auto pt-16 pb-8 lg:pt-24 lg:pb-10">
<div className="pointer-events-none absolute bottom-0 right-0 h-36 w-36 border border-primary-foreground/35 sm:right-2 lg:right-10" />
<div className="relative max-w-xl">
<p className="text-[clamp(4rem,10vw,9rem)] font-black uppercase leading-[0.75] tracking-normal">
01
@@ -131,7 +131,7 @@ const LandingHeroSection = () => {
<a
href="#kontakt"
className="group relative z-10 mt-auto inline-flex w-fit shrink-0 items-center gap-3 border border-primary-foreground px-5 py-4 text-sm font-semibold uppercase tracking-[0.18em] transition hover:bg-primary-foreground hover:text-primary"
className="group relative z-10 inline-flex w-fit shrink-0 items-center gap-3 border border-primary-foreground px-5 py-4 text-sm font-semibold uppercase tracking-[0.18em] transition hover:bg-primary-foreground hover:text-primary"
>
Projekt anfragen
<ArrowUpRight className="size-5 transition group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />

View File

@@ -0,0 +1,38 @@
import { run } from "vanilla-cookieconsent";
import "vanilla-cookieconsent/dist/cookieconsent.css";
const bannerText =
"Diese Website setzt keine Cookies. Wir erfassen ausschließlich anonymisierte Analytics-Daten, die keinen Rückschluss auf einzelne Nutzer zulassen.";
export function initCookieInfoBanner(): void {
if (typeof globalThis.window === "undefined") return;
void run({
hideFromBots: false,
categories: {
necessary: {
enabled: true,
readOnly: true,
},
},
cookie: {
name: "cc_notice",
useLocalStorage: true,
expiresAfterDays: 365,
},
language: {
default: "de",
translations: {
de: {
consentModal: {
title: "Datenschutzhinweis",
description: bannerText,
acceptNecessaryBtn: "Verstanden",
},
},
},
},
}).catch((err) => {
console.error("[CookieBanner]", err);
});
}

View File

@@ -22,5 +22,9 @@ import "@/styles/global.css";
<LandingHeroSection client:media="(min-width: 1024px)" />
<LandingRest />
</main>
<script>
import { initCookieInfoBanner } from "@/lib/cookie-banner-info";
initCookieInfoBanner();
</script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
import { readFile } from "node:fs/promises";
import test from "node:test";
import assert from "node:assert/strict";
const sourcePath = new URL("../src/lib/cookie-banner-info.ts", import.meta.url);
test("cookie banner uses the native CookieConsent close behavior", async () => {
const source = await readFile(sourcePath, "utf8");
assert.match(source, /from "vanilla-cookieconsent"/);
assert.match(source, /acceptNecessaryBtn: "Verstanden"/);
assert.doesNotMatch(source, /motion\/mini/);
assert.doesNotMatch(source, /cookie-info-fab/);
});