Blocks

Cookie Consent 4

Cookie Consent
Modal
Cookie Consent 4

Code

relume-cookie4.tsx
"use client";

import { useState, useEffect } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Close, Cookie } from "relume-icons";

type CookieConsent = {
  version: string;
  essential: true;
  marketing: boolean;
  personalization: boolean;
  analytics: boolean;
  timestamp: number;
};

type CookieBannerProps = {
  visible: boolean;
  onAccept: () => void;
  onDecline: () => void;
  onPreferences: () => void;
  onClose: () => void;
};

const CONSENT_VERSION = "1.0";

const getDefaultConsent = (): CookieConsent => ({
  version: CONSENT_VERSION,
  essential: true,
  marketing: false,
  personalization: false,
  analytics: false,
  timestamp: Date.now(),
});

const validateConsent = (consent: unknown): consent is CookieConsent => {
  if (typeof consent !== "object" || consent === null) return false;

  const requiredKeys = [
    "version",
    "essential",
    "marketing",
    "personalization",
    "analytics",
    "timestamp",
  ];
  return (
    requiredKeys.every((key) => key in consent) &&
    typeof (consent as CookieConsent).version === "string" &&
    typeof (consent as CookieConsent).essential === "boolean" &&
    typeof (consent as CookieConsent).marketing === "boolean" &&
    typeof (consent as CookieConsent).personalization === "boolean" &&
    typeof (consent as CookieConsent).analytics === "boolean" &&
    typeof (consent as CookieConsent).timestamp === "number"
  );
};

export const Cookie4 = () => {
  const [dialogOpen, setDialogOpen] = useState(false);
  const [bannerVisible, setBannerVisible] = useState(false);
  const [showPreferencesButton, setShowPreferencesButton] = useState(false);
  const [consent, setConsent] = useState<CookieConsent>(getDefaultConsent());
  const [hasConsentBeenSet, setHasConsentBeenSet] = useState(false);
  const [isBannerDismissed, setIsBannerDismissed] = useState(false);

  const applyCookieSettings = async (newConsent: CookieConsent): Promise<boolean> => {
    try {
      // Apply essential cookies (always enabled)
      // Add your essential cookies logic here

      // Apply optional cookies based on consent
      if (newConsent.marketing) {
        // Enable marketing cookies
        // Add your marketing cookies logic here
      }

      if (newConsent.analytics) {
        // Enable analytics cookies
        // Add your analytics cookies logic here
      }

      if (newConsent.personalization) {
        // Enable personalization cookies
        // Add your personalization cookies logic here
      }

      return true;
    } catch (error) {
      console.error("Error applying cookie settings:", error);
      return false;
    }
  };

  const handleConsentUpdate = async (newConsent: CookieConsent) => {
    const success = await applyCookieSettings(newConsent);
    if (success) {
      try {
        localStorage.setItem("cookieConsent", JSON.stringify(newConsent));
        setConsent(newConsent);
        setBannerVisible(false);
        setShowPreferencesButton(true);
        setHasConsentBeenSet(true);
      } catch (error) {
        console.error("Error saving cookie consent:", error);
      }
    } else {
      console.error("Failed to apply cookie settings");
    }
  };

  useEffect(() => {
    setDialogOpen(false);
    try {
      const savedConsent = localStorage.getItem("cookieConsent");
      if (savedConsent) {
        const parsed = JSON.parse(savedConsent);
        if (validateConsent(parsed)) {
          if (parsed.version !== CONSENT_VERSION) {
            setConsent(getDefaultConsent());
            setBannerVisible(true);
            setHasConsentBeenSet(false);
            return;
          }
          setConsent(parsed);
          setBannerVisible(false);
          setShowPreferencesButton(true);
          setHasConsentBeenSet(true);
        } else {
          throw new Error("Invalid consent structure");
        }
      } else {
        setConsent(getDefaultConsent());
        setBannerVisible(true);
      }
    } catch (error) {
      console.error("Error loading cookie consent:", error);
      localStorage.removeItem("cookieConsent");
      setConsent(getDefaultConsent());
      setBannerVisible(true);
    }
  }, []);

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const newConsent = {
      ...consent,
      timestamp: Date.now(),
    };
    await handleConsentUpdate(newConsent);
    setDialogOpen(false);
  };

  const handleAcceptAll = async () => {
    const newConsent: CookieConsent = {
      ...getDefaultConsent(),
      marketing: true,
      personalization: true,
      analytics: true,
      timestamp: Date.now(),
    };
    await handleConsentUpdate(newConsent);
    setDialogOpen(false);
  };

  const handleRejectAll = async () => {
    const newConsent: CookieConsent = {
      ...getDefaultConsent(),
      timestamp: Date.now(),
    };
    await handleConsentUpdate(newConsent);
    setDialogOpen(false);
  };

  const handleDialogOpenChange = (open: boolean) => {
    setDialogOpen(open);
    if (!open && !hasConsentBeenSet) {
      setBannerVisible(!isBannerDismissed);
      setShowPreferencesButton(isBannerDismissed);
    }
  };

  const handlePreferences = () => {
    setBannerVisible(false);
    setDialogOpen(true);
  };

  const handleCloseBanner = () => {
    setBannerVisible(false);
    setIsBannerDismissed(true);
    setShowPreferencesButton(true);
  };

  return (
    <section>
      <Dialog open={dialogOpen} onOpenChange={handleDialogOpenChange}>
        <AnimatePresence>
          {showPreferencesButton && (
            <motion.div
              initial={{ opacity: 0, y: 50 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: 50 }}
              transition={{ type: "spring", stiffness: 100, damping: 20 }}
              className="fixed bottom-4 left-4 md:bottom-8 md:left-8"
            >
              <DialogTrigger asChild>
                <Button
                  variant="link"
                  size="link"
                  iconLeft={<Cookie className="size-6 text-scheme-text" />}
                >
                  Cookie preferences
                </Button>
              </DialogTrigger>
            </motion.div>
          )}
        </AnimatePresence>
        <DialogContent
          closeIconPosition="inside"
          overlayClassName="bg-neutral-darkest/60"
          className="h-fit max-h-[70vh] w-[90%] max-w-lg overflow-scroll bg-white p-8 sm:w-[95%] md:p-10 lg:w-full lg:p-12"
        >
          <form onSubmit={handleSubmit}>
            <h2 className="mb-6 text-center text-h4 font-bold">Privacy Preferences</h2>
            <div className="grid grid-cols-1 items-start gap-y-6 py-6">
              <div className="flex items-center justify-between">
                <Label className="text-h6 font-bold">Essential</Label>
                <p className="font-semibold">Required</p>
              </div>
              <div className="h-px w-full bg-scheme-border" />
              <div className="flex items-center justify-between">
                <Label className="text-h6 font-bold">Marketing cookies</Label>
                <Checkbox
                  id="marketing"
                  checked={consent.marketing}
                  onCheckedChange={(checked: boolean) =>
                    setConsent((prev) => ({ ...prev, marketing: checked }))
                  }
                />
              </div>
              <div className="h-px w-full bg-scheme-border" />
              <div className="flex items-center justify-between">
                <Label className="text-h6 font-bold">Personalization cookies</Label>
                <Checkbox
                  id="personalization"
                  checked={consent.personalization}
                  onCheckedChange={(checked: boolean) =>
                    setConsent((prev) => ({ ...prev, personalization: checked }))
                  }
                />
              </div>
              <div className="h-px w-full bg-scheme-border" />
              <div className="flex items-center justify-between">
                <Label className="text-h6 font-bold">Analytics cookies</Label>
                <Checkbox
                  id="analytics"
                  checked={consent.analytics}
                  onCheckedChange={(checked: boolean) =>
                    setConsent((prev) => ({ ...prev, analytics: checked }))
                  }
                />
              </div>
            </div>
            <div className="h-px w-full bg-scheme-border" />
            <div className="mt-8 flex flex-col gap-4">
              <div className="flex flex-wrap gap-4 md:flex-nowrap">
                <Button onClick={handleRejectAll} variant="secondary" className="w-full">
                  Reject all cookies
                </Button>
                <Button onClick={handleAcceptAll} className="w-full">
                  Accept all cookies
                </Button>
              </div>
              <Button type="submit" className="w-full">
                Save preferences
              </Button>
            </div>
          </form>
        </DialogContent>
      </Dialog>
      <CookieBanner
        visible={bannerVisible}
        onAccept={handleAcceptAll}
        onDecline={handleRejectAll}
        onPreferences={handlePreferences}
        onClose={handleCloseBanner}
      />
    </section>
  );
};

const CookieBanner = ({
  visible,
  onAccept,
  onDecline,
  onPreferences,
  onClose,
}: CookieBannerProps) => {
  if (!visible) return null;

  return (
    <AnimatePresence>
      {visible && (
        <motion.div
          initial={{ opacity: 0, y: 50 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: 50 }}
          transition={{ type: "spring", stiffness: 100, damping: 20, duration: 0.3 }}
          className="absolute right-8 bottom-8 z-[998] flex w-[calc(100%-4rem)] max-w-md items-start justify-between gap-4 border border-scheme-border bg-scheme-background px-6 py-8 md:pr-12 md:pl-8"
        >
          <div className="flex flex-col flex-wrap gap-9 md:gap-8 lg:flex-nowrap">
            <p className="pr-4">
              By clicking "Accept all cookies", you agree to the storing of cookies on your device
              to enhance site navigation, analyze site usage, and assist in our marketing efforts.
              View our{" "}
              <a href="#" className="underline">
                Privacy Policy
              </a>{" "}
              for more information.
            </p>
            <div className="grid grid-cols-1 items-center justify-center gap-4">
              <div className="grid w-full grid-cols-1 gap-4 md:grid-cols-2">
                <Button onClick={onDecline}>Reject all cookies</Button>
                <Button onClick={onAccept}>Accept all cookies</Button>
              </div>
              <Button variant="secondary" onClick={onPreferences} className="w-full">
                Manage preferences
              </Button>
            </div>
          </div>
          <Button variant="link" size="link" onClick={onClose} className="absolute top-2 right-2">
            <Close className="size-6" />
          </Button>
        </motion.div>
      )}
    </AnimatePresence>
  );
};

npm i @radix-ui/react-checkbox @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-radio-group @radix-ui/react-select @radix-ui/react-slot @radix-ui/react-switch class-variance-authority@0.7.1 clsx@^2.1.1 motion@^12.15.0 relume-icons@1.3.0 tailwind-merge@^2.2.2