Skip to content

Commit eab516d

Browse files
feaf: add cookies banner
1 parent 01434bd commit eab516d

File tree

11 files changed

+1025
-15
lines changed

11 files changed

+1025
-15
lines changed

app/components/CookieBanner.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"use client"
2+
3+
import { useEffect, useState, useCallback } from "react"
4+
import Link from "next/link"
5+
import Cookies from "js-cookie"
6+
import { Button } from "@/components/ui/button"
7+
8+
const COOKIE_KEY = "dsh_cookie_consent"
9+
const COOKIE_MAX_DAYS = 365
10+
11+
export function useCookieConsent() {
12+
const [consent, setConsentState] = useState<"all" | "none" | null>(() => {
13+
if (typeof window === "undefined") return null
14+
return (Cookies.get(COOKIE_KEY) as "all" | "none") ?? null
15+
})
16+
17+
useEffect(() => {
18+
const onStorage = () => {
19+
setConsentState((Cookies.get(COOKIE_KEY) as "all" | "none") ?? null)
20+
}
21+
const onCustom = (e: Event) => {
22+
const detail = (e as CustomEvent).detail as "all" | "none"
23+
setConsentState(
24+
detail ?? (Cookies.get(COOKIE_KEY) as "all" | "none") ?? null
25+
)
26+
}
27+
28+
window.addEventListener("storage", onStorage)
29+
window.addEventListener("dsh_cookie_consent", onCustom as EventListener)
30+
return () => {
31+
window.removeEventListener("storage", onStorage)
32+
window.removeEventListener(
33+
"dsh_cookie_consent",
34+
onCustom as EventListener
35+
)
36+
}
37+
}, [])
38+
39+
const setConsent = useCallback((value: "all" | "none") => {
40+
Cookies.set(COOKIE_KEY, value, {
41+
expires: COOKIE_MAX_DAYS,
42+
sameSite: "Lax",
43+
})
44+
try {
45+
// trigger storage event in other tabs
46+
localStorage.setItem(COOKIE_KEY, value)
47+
localStorage.removeItem(COOKIE_KEY)
48+
} catch {
49+
/* ignore */
50+
}
51+
// notify same-tab listeners
52+
window.dispatchEvent(
53+
new CustomEvent("dsh_cookie_consent", { detail: value })
54+
)
55+
setConsentState(value)
56+
}, [])
57+
58+
return { consent, setConsent }
59+
}
60+
61+
export default function CookieBanner() {
62+
const { consent, setConsent } = useCookieConsent()
63+
const [isVisible, setIsVisible] = useState(false)
64+
65+
useEffect(() => {
66+
const timer = setTimeout(() => {
67+
if (consent === null) setIsVisible(true)
68+
}, 700)
69+
return () => clearTimeout(timer)
70+
}, [consent])
71+
72+
if (!isVisible) return null
73+
74+
return (
75+
<div className="fixed bottom-0 left-0 right-0 z-50 animate-slideUp">
76+
<div className="bg-white border-t border-gray-200 shadow-lg">
77+
<div className="max-w-7xl mx-auto p-4 md:p-6">
78+
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
79+
<div className="flex-1">
80+
<p className="text-base text-[#1E1F1E] mb-2">
81+
We use cookies to improve your browsing experience and to
82+
display embedded YouTube videos. Some cookies are set by third
83+
parties such as Google (Analytics and YouTube).
84+
</p>
85+
</div>
86+
87+
<div className="flex flex-col sm:flex-row gap-3 items-center">
88+
<Button
89+
onClick={() => {
90+
setConsent("all")
91+
setIsVisible(false)
92+
}}
93+
className="bg-[#2D6A4F] text-white hover:bg-[#1D593F] rounded-full w-full sm:w-auto"
94+
>
95+
Accept all
96+
</Button>
97+
98+
<Button
99+
onClick={() => {
100+
setConsent("none")
101+
setIsVisible(false)
102+
}}
103+
variant="outline"
104+
className="border-[#17412C] text-[#0D261A] rounded-full w-full sm:w-auto"
105+
>
106+
Reject all
107+
</Button>
108+
109+
<Link
110+
href="/cookies"
111+
className="text-[#0D6E4B] underline text-sm"
112+
>
113+
More information
114+
</Link>
115+
</div>
116+
</div>
117+
</div>
118+
</div>
119+
</div>
120+
)
121+
}

app/components/GoogleAnalytics.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
"use client"
22

33
import Script from "next/script"
4+
import { useEffect, useState } from "react"
5+
import { useCookieConsent } from "./CookieBanner"
46

57
const GA_TRACKING_ID = "G-90PN28ZXNP"
68

79
export default function GoogleAnalytics() {
10+
const { consent } = useCookieConsent()
11+
const [isEnabled, setIsEnabled] = useState(false)
12+
13+
useEffect(() => {
14+
setIsEnabled(consent === "all")
15+
}, [consent])
16+
17+
if (!isEnabled) return null
18+
819
return (
920
<>
1021
<Script

app/components/Landing.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import QuestionaireFilter from "../components/Questionairefilter"
1414
import { ToolCategoriesDrawer } from "../components/categories-drawer"
1515
import { useMobile } from "../hooks/use-mobile"
1616
import { FilterState, Tool } from "../types"
17+
import YouTubeEmbed from "./YouTubeEmbed"
1718

1819
export default function Landing() {
1920
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
@@ -212,19 +213,12 @@ export default function Landing() {
212213
</div>
213214
</section>
214215

216+
{/* Replace the iframe with YouTubeEmbed component */}
215217
<div className="flex justify-center bg-[#F9FBFA] pb-4">
216-
<div className="w-full max-w-[560px] aspect-video rounded-lg overflow-hidden shadow-lg">
217-
<iframe
218-
width="560"
219-
height="315"
220-
src="https://www.youtube.com/embed/SWFvqJPVJhQ"
221-
title="YouTube video"
222-
frameBorder="0"
223-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
224-
allowFullScreen
225-
className="w-full h-full"
226-
></iframe>
227-
</div>
218+
<YouTubeEmbed
219+
videoId="SWFvqJPVJhQ"
220+
className="w-full max-w-[560px]"
221+
/>
228222
</div>
229223

230224
{/* Why Digitalization Matters Section */}
@@ -354,9 +348,9 @@ export default function Landing() {
354348
</div>
355349

356350
<div className="flex justify-center gap-4 text-sm text-[#091A12] underline font-bold">
357-
<Link href="#">Imprint</Link>
358-
<Link href="#">Privacy Policy</Link>
359-
<Link href="#">Cookies</Link>
351+
<Link href="/legal-disclosure">Legal Disclosure</Link>
352+
<Link href="/privacy-policy">Privacy Policy</Link>
353+
<Link href="/cookies">Cookies</Link>
360354
</div>
361355
</div>
362356
</footer>

app/components/YouTubeEmbed.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client"
2+
3+
import { useCookieConsent } from "./CookieBanner"
4+
import { Button } from "@/components/ui/button"
5+
6+
interface YouTubeEmbedProps {
7+
videoId: string
8+
className?: string
9+
}
10+
11+
export default function YouTubeEmbed({
12+
videoId,
13+
className = "",
14+
}: YouTubeEmbedProps) {
15+
const { consent, setConsent } = useCookieConsent()
16+
17+
// if consent not granted, show placeholder with an Accept button that sets consent and immediately loads the video
18+
if (consent !== "all") {
19+
return (
20+
<div className={`bg-gray-50 rounded-lg p-6 text-center ${className}`}>
21+
<p className="text-[#1E1F1E] mb-4">
22+
This content is currently not available due to your cookie
23+
preferences.
24+
</p>
25+
26+
<div className="flex justify-center gap-3">
27+
<Button
28+
onClick={() => setConsent("all")}
29+
className="bg-[#2D6A4F] text-white rounded-full"
30+
>
31+
Accept cookies and show video
32+
</Button>
33+
34+
<Button
35+
onClick={() => setConsent("none")}
36+
variant="outline"
37+
className="border-[#17412C] text-[#0D261A] rounded-full"
38+
>
39+
Reject
40+
</Button>
41+
</div>
42+
</div>
43+
)
44+
}
45+
46+
// consent === "all" -> render iframe immediately (no refresh needed)
47+
return (
48+
<div
49+
className={`aspect-video rounded-lg overflow-hidden shadow-lg ${className}`}
50+
>
51+
<iframe
52+
src={`https://www.youtube.com/embed/${videoId}`}
53+
title="YouTube video"
54+
className="w-full h-full"
55+
frameBorder="0"
56+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
57+
allowFullScreen
58+
/>
59+
</div>
60+
)
61+
}

0 commit comments

Comments
 (0)