add cookie component

This commit is contained in:
Nikita 2026-09-21 22:04:01 +05:00
parent d7048b97c7
commit 690c4b20a9
6 changed files with 258 additions and 0 deletions

View file

@ -1,8 +1,10 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { CookieConsent } from "@/widgets/cookie-consent";
import { Cursor } from "@/widgets/cursor";
import { Preloader } from "@/widgets/preloader";
import { ScrollCounter } from "@/widgets/scroll-counter";
import { YandexMetrika } from "@/widgets/yandex-metrika";
import "@/static/globals.css";
const geistSans = Geist({
@ -31,6 +33,8 @@ const RootLayout = ({ children }: LayoutProps<"/">) => {
<Cursor />
<ScrollCounter />
{children}
<CookieConsent />
<YandexMetrika />
</body>
</html>
);

View file

@ -0,0 +1,115 @@
.banner {
position: fixed;
right: var(--container-padding);
bottom: var(--space-md);
left: var(--container-padding);
z-index: 9980;
display: flex;
justify-content: center;
pointer-events: none;
}
.content {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-md);
width: min(720px, 100%);
padding: 18px;
border: 1px solid var(--color-line);
border-radius: 20px;
background: rgba(10, 10, 10, 0.92);
color: var(--color-text);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
pointer-events: auto;
animation: bannerIn 0.35s ease;
}
.text {
display: flex;
flex-direction: column;
gap: 4px;
}
.title {
margin: 0;
font-size: var(--fs-small);
font-weight: 700;
line-height: 1.2;
}
.description {
max-width: 52ch;
margin: 0;
color: var(--color-muted);
font-size: var(--fs-small);
line-height: 1.5;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0 22px;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.86);
border-radius: 100px;
background: var(--color-text);
color: var(--color-bg);
font: inherit;
font-size: var(--fs-small);
font-weight: 700;
cursor: pointer;
transition:
transform 0.25s ease,
opacity 0.25s ease;
}
.button:hover {
opacity: 0.86;
transform: translateY(-1px);
}
@keyframes bannerIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
@media (max-width: 640px) {
.content {
align-items: stretch;
flex-direction: column;
}
.button {
width: 100%;
}
}

View file

@ -0,0 +1,5 @@
export {
COOKIE_CONSENT_EVENT,
COOKIE_CONSENT_KEY,
default as CookieConsent,
} from "./ui/CookieConsent";

View file

@ -0,0 +1,48 @@
"use client";
import { useEffect, useState } from "react";
import styles from "@/static/cookie-consent/cookie-consent.module.css";
export const COOKIE_CONSENT_KEY = "autli-cookie-consent";
export const COOKIE_CONSENT_EVENT = "autli-cookie-consent-accepted";
const CookieConsent = () => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const timer = window.setTimeout(() => {
setVisible(window.localStorage.getItem(COOKIE_CONSENT_KEY) !== "accepted");
}, 0);
return () => window.clearTimeout(timer);
}, []);
const acceptCookies = () => {
window.localStorage.setItem(COOKIE_CONSENT_KEY, "accepted");
window.dispatchEvent(new Event(COOKIE_CONSENT_EVENT));
setVisible(false);
};
if (!visible) return null;
return (
<section className={styles.banner} aria-label="Согласие на использование cookie">
<div className={styles.content}>
<div className={styles.text}>
<p className={styles.title}>Cookie-файлы</p>
<p className={styles.description}>
Я использую cookie и Яндекс Метрику, чтобы понимать, как работает
сайт, и улучшать его для посетителей.
</p>
</div>
<button type="button" className={styles.button} onClick={acceptCookies}>
Принять
</button>
</div>
</section>
);
};
export default CookieConsent;

View file

@ -0,0 +1 @@
export { default as YandexMetrika } from "./ui/YandexMetrika";

View file

@ -0,0 +1,85 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import Script from "next/script";
import {
COOKIE_CONSENT_EVENT,
COOKIE_CONSENT_KEY,
} from "@/widgets/cookie-consent";
declare global {
interface Window {
ym?: (
counterId: number,
action: "init" | "hit",
options?: Record<string, unknown> | string
) => void;
}
}
const counterId = Number(process.env.NEXT_PUBLIC_YANDEX_METRIKA_ID);
const YandexMetrika = () => {
const pathname = usePathname();
const [accepted, setAccepted] = useState(false);
const previousUrl = useRef<string | null>(null);
useEffect(() => {
const timer = window.setTimeout(() => {
setAccepted(window.localStorage.getItem(COOKIE_CONSENT_KEY) === "accepted");
}, 0);
const onAccepted = () => setAccepted(true);
window.addEventListener(COOKIE_CONSENT_EVENT, onAccepted);
return () => {
window.clearTimeout(timer);
window.removeEventListener(COOKIE_CONSENT_EVENT, onAccepted);
};
}, []);
useEffect(() => {
if (!accepted || !counterId || !window.ym) return;
const url = window.location.href;
if (previousUrl.current === null) {
previousUrl.current = url;
return;
}
if (previousUrl.current === url) return;
previousUrl.current = url;
window.ym(counterId, "hit", url);
}, [accepted, pathname]);
if (!accepted || !counterId) return null;
return (
<Script
id="yandex-metrika"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(${counterId}, "init", {
accurateTrackBounce: true,
clickmap: true,
defer: true,
trackLinks: true,
webvisor: true
});
`,
}}
/>
);
};
export default YandexMetrika;