Blocks

Cookie Consent 1

Cookie Consent
Modal
Cookie Consent 1

Code

relume-cookie1.tsx
"use client";

import { useState, useEffect } from "react";
import { AnimatePresence, motion } from "motion/react";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
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 Cookie1 = () => {
  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>
      <div className="relative h-screen">
        <div className="container max-w-lg">
          <Dialog open={dialogOpen} onOpenChange={handleDialogOpenChange}>
            <AnimatePresence>
              {showPreferencesButton && (
                <motion.div
                  initial={{ opacity: 0, y: 100 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: 100 }}
                  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}>
                <div>
                  <h2 className="mb-3 text-h4 font-bold md:mb-4">Privacy Preferences</h2>
                  <p>
                    When you visit websites, they may store or retrieve data in your browser. This
                    storage is often necessary for the basic functionality of the website. The
                    storage may be used for marketing, analytics, and personalization of the site,
                    such as storing your preferences. Privacy is important to us, so you have the
                    option of disabling certain types of storage that may not be necessary for the
                    basic functioning of the website. Blocking categories may impact your experience
                    on the website.
                  </p>
                  <div className="mt-8 flex flex-wrap justify-start gap-4">
                    <Button variant="secondary" onClick={handleRejectAll}>
                      Reject all cookies
                    </Button>
                    <Button onClick={handleAcceptAll}>Accept all cookies</Button>
                  </div>
                  <div className="mt-12 h-px w-full bg-scheme-border" />
                </div>
                <div className="mt-12">
                  <div className="grid grid-cols-1 items-center gap-6">
                    <h2 className="text-h4 font-bold">Manage consent preferences by category</h2>
                    <div className="grid grid-cols-1 gap-2">
                      <div className="flex items-center justify-between">
                        <Label className="text-h6 font-bold">Essential</Label>
                        <p className="font-semibold">Always active</p>
                      </div>
                      <p>These items are required to enable basic website functionality.</p>
                    </div>
                    <div className="grid grid-cols-1 gap-2">
                      <div className="flex items-center justify-between">
                        <Label className="text-h6 font-bold">Marketing</Label>
                        <Switch
                          id="marketing-switch"
                          checked={consent.marketing}
                          onCheckedChange={(checked) =>
                            setConsent((prev) => ({ ...prev, marketing: checked }))
                          }
                        />
                      </div>
                      <p>
                        These items are used to deliver advertising that is more relevant to you and
                        your interests. They may also be used to limit the number of times you see
                        an advertisement and measure the effectiveness of advertising campaigns.
                        Advertising networks usually place them with the website operator’s
                        permission.
                      </p>
                    </div>
                    <div className="grid grid-cols-1 gap-2">
                      <div className="flex items-center justify-between">
                        <Label className="text-h6 font-bold">Personalization</Label>
                        <Switch
                          id="personalization-switch"
                          checked={consent.personalization}
                          onCheckedChange={(checked) =>
                            setConsent((prev) => ({ ...prev, personalization: checked }))
                          }
                        />
                      </div>
                      <p>
                        These items allow the website to remember choices you make (such as your
                        user name, language, or the region you are in) and provide enhanced, more
                        personal features. For example, a website may provide you with local weather
                        reports or traffic news by storing data about your current location.
                      </p>
                    </div>
                    <div className="grid grid-cols-1 gap-2">
                      <div className="flex items-center justify-between">
                        <Label className="text-h6 font-bold">Analytics</Label>
                        <Switch
                          id="analytics-switch"
                          checked={consent.analytics}
                          onCheckedChange={(checked) =>
                            setConsent((prev) => ({ ...prev, analytics: checked }))
                          }
                        />
                      </div>
                      <p>
                        These items help the website operator understand how its website performs,
                        how visitors interact with the site, and whether there may be technical
                        issues. This storage type usually doesn’t collect information that
                        identifies a visitor.
                      </p>
                    </div>
                    <div className="mt-8 flex flex-wrap justify-end gap-4">
                      <Button onClick={() => setDialogOpen(false)} variant="secondary">
                        Cancel
                      </Button>
                      <Button type="submit">Save preferences</Button>
                    </div>
                  </div>
                </div>
              </form>
            </DialogContent>
          </Dialog>
        </div>
      </div>
      <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-xl flex-col items-start justify-start gap-8 overflow-hidden rounded-none border border-scheme-border bg-scheme-background px-6 py-8 lg:flex-row lg:items-center lg:pr-8 lg:pl-12"
        >
          <p>
            By clicking "Accept", 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="flex flex-none flex-wrap items-center gap-4 self-end lg:self-auto">
            <Button variant="link" size="link" onClick={onPreferences}>
              Preferences
            </Button>
            <Button variant="secondary" onClick={onDecline}>
              Decline
            </Button>
            <Button onClick={onAccept}>Accept</Button>
          </div>
          <Button
            variant="link"
            size="link"
            onClick={onClose}
            className="absolute top-2 right-2 lg:static"
          >
            <Close className="size-6 lg:size-8" />
          </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