mirror of
https://github.com/XFox111/my-website.git
synced 2026-04-22 07:28:01 +03:00
feat!: added cookie consent banner and management system
This commit is contained in:
@@ -12,3 +12,4 @@ SMTP_TO_EMAIL=email # Email to which emails will be sent
|
|||||||
DOMAIN_NAME=example.com # Your domain name
|
DOMAIN_NAME=example.com # Your domain name
|
||||||
RESUME_URL=URL # Location of the resume PDF
|
RESUME_URL=URL # Location of the resume PDF
|
||||||
CLARITY_ID=string # Clarity Analytics ID (optional, remove to disable)
|
CLARITY_ID=string # Clarity Analytics ID (optional, remove to disable)
|
||||||
|
NEXT_PUBLIC_CLARITY_CONSENT=1 # 1 if you need to request explicit consent from user, 0 if not (requires CLARITY_ID)
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
@import "../theme.scss";
|
||||||
|
|
||||||
|
.banner
|
||||||
|
{
|
||||||
|
@include flex(row);
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 8%;
|
||||||
|
background-color: $colorNeutralBackground2;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
.learnMore
|
||||||
|
{
|
||||||
|
gap: $spacingL;
|
||||||
|
padding: $spacingMNudge $spacingL;
|
||||||
|
background-size: $spacingMNudge 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
p
|
||||||
|
{
|
||||||
|
color: $colorNeutralForeground2;
|
||||||
|
@include body1;
|
||||||
|
|
||||||
|
span
|
||||||
|
{
|
||||||
|
color: $colorNeutralForeground1;
|
||||||
|
@include body2($fontFamilyBaseAlt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible
|
||||||
|
{
|
||||||
|
|
||||||
|
p,
|
||||||
|
p > span
|
||||||
|
{
|
||||||
|
color: $colorNeutralForegroundInverted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dismiss
|
||||||
|
{
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls
|
||||||
|
{
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px)
|
||||||
|
{
|
||||||
|
> button
|
||||||
|
{
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px)
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
&:not(:has(> .dismiss))
|
||||||
|
{
|
||||||
|
flex-flow: column;
|
||||||
|
|
||||||
|
.learnMore
|
||||||
|
{
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls > button:last-child
|
||||||
|
{
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { acceptCookies, dismissCookies, getCookieChoice, rejectCookies, requireExcplicitConsent } from "@/_utils/analytics/client";
|
||||||
|
import { Dismiss24Regular } from "@fluentui/react-icons";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import Button from "./Button";
|
||||||
|
import cls from "./CookieBanner.module.scss";
|
||||||
|
|
||||||
|
const CookieBanner: React.FC = () =>
|
||||||
|
{
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
const choice = getCookieChoice();
|
||||||
|
setVisible(choice === "none");
|
||||||
|
|
||||||
|
// Since Clarity cookies expiration dates extend well beyond 60 days,
|
||||||
|
// we need to terminate them manually once our consent tracking cookie expires.
|
||||||
|
if (choice !== "accepted")
|
||||||
|
window.clarity?.("consent", false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const accept = useCallback(() =>
|
||||||
|
{
|
||||||
|
acceptCookies();
|
||||||
|
setVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reject = useCallback(() =>
|
||||||
|
{
|
||||||
|
rejectCookies();
|
||||||
|
setVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dismiss = useCallback(() =>
|
||||||
|
{
|
||||||
|
dismissCookies();
|
||||||
|
setVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!visible)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ `cookie-banner ${cls.banner}` }>
|
||||||
|
<Button
|
||||||
|
as="next" href="/attribution"
|
||||||
|
className={ cls.learnMore }
|
||||||
|
aria-label="We use cookies for analytics purposes. Click to learn more"
|
||||||
|
icon={ <span>🍪</span> }>
|
||||||
|
|
||||||
|
<p aria-hidden>
|
||||||
|
<span>We use cookies for analytics purposes</span><br />
|
||||||
|
Click to learn more
|
||||||
|
</p>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{ requireExcplicitConsent ?
|
||||||
|
<div className={ cls.controls }>
|
||||||
|
<Button onClick={ accept }>Accept</Button>
|
||||||
|
<Button onClick={ reject }>Reject</Button>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<Button
|
||||||
|
title="Dismiss" icon={ <Dismiss24Regular /> }
|
||||||
|
onClick={ dismiss } className={ cls.dismiss } />
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CookieBanner;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Button from "@/_components/Button";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getCookieChoice, rejectCookies } from "../_utils/analytics/client";
|
||||||
|
|
||||||
|
const RevokeConsentButton: React.FC = () =>
|
||||||
|
{
|
||||||
|
const [hasConsent, setHasConsent] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
console.log("getCookieChoice", getCookieChoice());
|
||||||
|
setHasConsent(getCookieChoice() === "accepted");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const revoke = useCallback(() =>
|
||||||
|
{
|
||||||
|
rejectCookies();
|
||||||
|
setHasConsent(false);
|
||||||
|
window.alert("Your consent has been revoked");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasConsent)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return <Button onClick={ revoke }>Revoke my consent</Button>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RevokeConsentButton;
|
||||||
Vendored
+55
@@ -0,0 +1,55 @@
|
|||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
declare global
|
||||||
|
{
|
||||||
|
interface Window
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Notify Clarity about user's cookie consent
|
||||||
|
* @param cmd - command
|
||||||
|
* @param consent - consent (default: true)
|
||||||
|
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api
|
||||||
|
*/
|
||||||
|
clarity?(cmd: "consent", consent?: boolean): void;
|
||||||
|
/**
|
||||||
|
* Set custom Clarity user identifier
|
||||||
|
* @param cmd - command
|
||||||
|
* @param customId - user identifier
|
||||||
|
* @param customSessionId - session identifier
|
||||||
|
* @param customPageId - page identifier
|
||||||
|
* @param friendlyName - user friendly name
|
||||||
|
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api
|
||||||
|
*/
|
||||||
|
clarity?(cmd: "identify", customId: string, customSessionId?: string, customPageId?: string, friendlyName?: string): void;
|
||||||
|
/**
|
||||||
|
* Add custom tag to Clarity session recording
|
||||||
|
* @param cmd - command
|
||||||
|
* @param key - tag
|
||||||
|
* @param value - value
|
||||||
|
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api
|
||||||
|
*/
|
||||||
|
clarity?(cmd: "set", key: string, value: string): void;
|
||||||
|
/**
|
||||||
|
* Add custom event to Clarity session recording
|
||||||
|
* @param cmd - command
|
||||||
|
* @param value - event name
|
||||||
|
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api
|
||||||
|
*/
|
||||||
|
clarity?(cmd: "event", value: string): void;
|
||||||
|
/**
|
||||||
|
* Prioritize current Clarity session recording
|
||||||
|
* @param cmd - command
|
||||||
|
* @param reason - reason for upgrade
|
||||||
|
* @see https://learn.microsoft.com/en-us/clarity/setup-and-installation/clarity-api
|
||||||
|
*/
|
||||||
|
clarity?(cmd: "upgrade", reason: string): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClarityProps =
|
||||||
|
{ cmd: "consent", consent?: boolean; } |
|
||||||
|
{ cmd: "identify", customId: string, customSessionId?: string, customPageId?: string, friendlyName?: string; } |
|
||||||
|
{ cmd: "set", key: string, value: string; } |
|
||||||
|
{ cmd: "event", value: string; } |
|
||||||
|
{ cmd: "upgrade", reason: string; };
|
||||||
|
|
||||||
|
export default global;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
export const acceptCookies = (): void =>
|
||||||
|
{
|
||||||
|
setCookie("CC", "1", 5184000); // 60 days
|
||||||
|
window.clarity?.("consent", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rejectCookies = (): void =>
|
||||||
|
{
|
||||||
|
setCookie("CC", "0", 86400); // 1 day
|
||||||
|
window.clarity?.("consent", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dismissCookies = (): void =>
|
||||||
|
{
|
||||||
|
setCookie("CC", "", 1209600); // 14 days
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireExcplicitConsent: boolean = process.env.NEXT_PUBLIC_CLARITY_CONSENT === "1";
|
||||||
|
|
||||||
|
export const getCookieChoice = (): "accepted" | "rejected" | "acknowledged" | "none" =>
|
||||||
|
{
|
||||||
|
switch (getCookie("CC"))
|
||||||
|
{
|
||||||
|
case "1": return "accepted";
|
||||||
|
case "0": return "rejected";
|
||||||
|
case "": return "acknowledged";
|
||||||
|
default: return "none";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function setCookie(name: string, value: string | number, maxAge: number): void
|
||||||
|
{
|
||||||
|
window.document.cookie = `${name}=${value}; max-age=${maxAge}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name: string): string | undefined
|
||||||
|
{
|
||||||
|
let cookieName = name + "=";
|
||||||
|
let rawCookies = decodeURIComponent(window.document.cookie);
|
||||||
|
let cookies = rawCookies.split(";");
|
||||||
|
|
||||||
|
for (const cookie of cookies)
|
||||||
|
if (cookie.trim().startsWith(cookieName))
|
||||||
|
return cookie.substring(cookieName.length);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -33,4 +33,10 @@
|
|||||||
padding: revert;
|
padding: revert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonRow
|
||||||
|
{
|
||||||
|
@include flex(row, wrap);
|
||||||
|
gap: $spacingS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Package from "@/../package.json";
|
import Package from "@/../package.json";
|
||||||
import Button from "@/_components/Button";
|
import Button from "@/_components/Button";
|
||||||
|
import RevokeConsentButton from "@/_components/RevokeConsentButton";
|
||||||
import { canonicalName, getTitle } from "@/_data/metadata";
|
import { canonicalName, getTitle } from "@/_data/metadata";
|
||||||
import ThirdPartyAttribution from "@/_data/ThirdPartyAttributiont";
|
import ThirdPartyAttribution from "@/_data/ThirdPartyAttributiont";
|
||||||
import { analyticsEnabled } from "@/_utils/analytics/server";
|
import { analyticsEnabled } from "@/_utils/analytics/server";
|
||||||
@@ -39,12 +40,19 @@ const AttributionPage: React.FC = () => (
|
|||||||
<p>
|
<p>
|
||||||
If "Do Not Track" option is enabled in your browser, the website will not load any tracking code.
|
If "Do Not Track" option is enabled in your browser, the website will not load any tracking code.
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
If you previously gave your consent to use cookies, you can revoke it by clicking "Revoke my consent" button on this page below (the button is available only if the consent was given). Recorded data will be deleted after 30 day retention period.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={ cls.buttonRow }>
|
||||||
|
<RevokeConsentButton />
|
||||||
<Button appearance="secondary"
|
<Button appearance="secondary"
|
||||||
href="https://learn.microsoft.com/clarity/faq#privacy" target="_blank"
|
href="https://learn.microsoft.com/clarity/faq#privacy" target="_blank"
|
||||||
iconAfter={ <ArrowRight24Regular /> }>
|
iconAfter={ <ArrowRight24Regular /> }>
|
||||||
|
|
||||||
Visit Clarity privacy FAQ
|
Visit Clarity privacy FAQ
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,3 +93,11 @@ main.not-found + footer > .illustration
|
|||||||
{
|
{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px)
|
||||||
|
{
|
||||||
|
body:has(.cookie-banner)
|
||||||
|
{
|
||||||
|
padding-bottom: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-3
@@ -1,8 +1,8 @@
|
|||||||
import { metadata as myMetadata } from "@/_data/metadata";
|
import { metadata as myMetadata } from "@/_data/metadata";
|
||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { headers as getHeaders } from "next/headers";
|
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
|
import CookieBanner from "./_components/CookieBanner";
|
||||||
import Footer from "./_components/Footer";
|
import Footer from "./_components/Footer";
|
||||||
import Header from "./_components/Header";
|
import Header from "./_components/Header";
|
||||||
import { canLoadAnalytics } from "./_utils/analytics/server";
|
import { canLoadAnalytics } from "./_utils/analytics/server";
|
||||||
@@ -23,8 +23,6 @@ export const metadata: Metadata = myMetadata;
|
|||||||
|
|
||||||
export default function RootLayout(props: PropsWithChildren)
|
export default function RootLayout(props: PropsWithChildren)
|
||||||
{
|
{
|
||||||
const headers = getHeaders();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" className={ fonts.map(i => i.variable).join(" ") }>
|
<html lang="en" className={ fonts.map(i => i.variable).join(" ") }>
|
||||||
{ canLoadAnalytics() &&
|
{ canLoadAnalytics() &&
|
||||||
@@ -32,6 +30,7 @@ export default function RootLayout(props: PropsWithChildren)
|
|||||||
<Script id="ms-clarity" src="/clarity.js" data-id={ process.env.CLARITY_ID } />
|
<Script id="ms-clarity" src="/clarity.js" data-id={ process.env.CLARITY_ID } />
|
||||||
}
|
}
|
||||||
<body>
|
<body>
|
||||||
|
{ canLoadAnalytics() && <CookieBanner /> }
|
||||||
<Header />
|
<Header />
|
||||||
{ props.children }
|
{ props.children }
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
Reference in New Issue
Block a user