1
0
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:
2024-08-21 00:22:49 +00:00
parent dceb2e44b7
commit 67222999a9
10 changed files with 330 additions and 17 deletions
+10 -9
View File
@@ -2,13 +2,14 @@
# Copy this file to .env, .env.local, or .env.production and fill in the values # Copy this file to .env, .env.local, or .env.production and fill in the values
# Mail credentials for redirecting form inquiries (see app/_utils/sendInquiry.ts) # Mail credentials for redirecting form inquiries (see app/_utils/sendInquiry.ts)
SMTP_HOST=mailserver # Address of your SMTP server SMTP_HOST=mailserver # Address of your SMTP server
SMTP_PORT=port # Port of your SMTP server SMTP_PORT=port # Port of your SMTP server
SMTP_USER=username # Username of your email bot account (usually same, as email address) SMTP_USER=username # Username of your email bot account (usually same, as email address)
SMTP_PASSWORD=password # Password of your email bot account SMTP_PASSWORD=password # Password of your email bot account
SMTP_FROM_EMAIL=email # Email address which will be displayed in "From" field SMTP_FROM_EMAIL=email # Email address which will be displayed in "From" field
SMTP_TO_EMAIL=email # Email to which emails will be sent 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)
+83
View File
@@ -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;
}
}
}
}
+74
View File
@@ -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;
+30
View File
@@ -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;
+55
View File
@@ -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;
+49
View File
@@ -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;
}
+6
View File
@@ -33,4 +33,10 @@
padding: revert; padding: revert;
} }
} }
.buttonRow
{
@include flex(row, wrap);
gap: $spacingS;
}
} }
+13 -5
View File
@@ -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>
<Button appearance="secondary" <p>
href="https://learn.microsoft.com/clarity/faq#privacy" target="_blank" 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.
iconAfter={ <ArrowRight24Regular /> }> </p>
Visit Clarity privacy FAQ <div className={ cls.buttonRow }>
</Button> <RevokeConsentButton />
<Button appearance="secondary"
href="https://learn.microsoft.com/clarity/faq#privacy" target="_blank"
iconAfter={ <ArrowRight24Regular /> }>
Visit Clarity privacy FAQ
</Button>
</div>
</section> </section>
} }
+8
View File
@@ -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
View File
@@ -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 />