TypeScript 93.3%
JavaScript 4.4%
CSS 2.3%
1/**2 * Author: Simon-Pierre Boucher3 * Contact: contact@spboucher.ai4 * Project: Hilmacorp.ai — Web Platform5 * File: components/CookieConsent.tsx6 * Description: Cookie consent banner — records the visitor's choice in a first-party cookie7 * and can be reopened at any time via the "hilmacorp:cookie-preferences" event8 */910"use client";1112import { useEffect, useState } from "react";13import Link from "next/link";1415export const COOKIE_CONSENT_NAME = "hilmacorp_cookie_consent";16export const COOKIE_PREFERENCES_EVENT = "hilmacorp:cookie-preferences";1718interface CookieStrings {19 title: string;20 body: string;21 acceptAll: string;22 essentialOnly: string;23 privacyLink: string;24 note: string;25}2627function readConsent(): string | null {28 const match = document.cookie.match(29 new RegExp(`(?:^|; )${COOKIE_CONSENT_NAME}=([^;]*)`)30 );31 return match ? decodeURIComponent(match[1]) : null;32}3334function writeConsent(value: "all" | "essential") {35 document.cookie = `${COOKIE_CONSENT_NAME}=${value};path=/;max-age=31536000;samesite=lax`;36}3738export default function CookieConsent({39 locale,40 strings,41}: {42 locale: "fr" | "en";43 strings: CookieStrings;44}) {45 const [visible, setVisible] = useState(false);4647 useEffect(() => {48 if (readConsent() === null) setVisible(true);49 const reopen = () => setVisible(true);50 window.addEventListener(COOKIE_PREFERENCES_EVENT, reopen);51 return () => window.removeEventListener(COOKIE_PREFERENCES_EVENT, reopen);52 }, []);5354 if (!visible) return null;5556 function choose(value: "all" | "essential") {57 writeConsent(value);58 setVisible(false);59 }6061 return (62 <div63 role="dialog"64 aria-modal="false"65 aria-label={strings.title}66 className="fixed inset-x-0 bottom-0 z-[60] px-4 pb-4 sm:px-6 sm:pb-6"67 >68 <div className="mx-auto max-w-3xl rounded-2xl border border-line-strong bg-card p-6 shadow-xl sm:p-7">69 <h2 className="font-display text-lg font-semibold text-ink">{strings.title}</h2>70 <p className="mt-2 text-sm leading-relaxed text-ink-soft">{strings.body}</p>71 <p className="mt-2 text-xs text-ink-faint">{strings.note}</p>72 <div className="mt-5 flex flex-wrap items-center gap-3">73 <button74 type="button"75 onClick={() => choose("all")}76 className="rounded-full bg-ink px-6 py-2.5 text-sm font-medium text-paper transition-colors hover:bg-accent-strong"77 >78 {strings.acceptAll}79 </button>80 <button81 type="button"82 onClick={() => choose("essential")}83 className="rounded-full border border-line-strong px-6 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent hover:text-accent"84 >85 {strings.essentialOnly}86 </button>87 <Link88 href={`/${locale}/privacy`}89 className="text-sm font-medium text-accent hover:text-accent-strong"90 >91 {strings.privacyLink}92 </Link>93 </div>94 </div>95 </div>96 );97}98