SPB Git forge

spb/hilmacorp

Public
1commits 1branches 0releases
29.2 MBsize
maindefault branch
19 days agolast push
TypeScript 93.3% JavaScript 4.4% CSS 2.3%
3.0 KB · 98 lines tsx
Raw Blame History
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