feat: KVKK/GDPR uyumlu cookie consent + Google Consent Mode v2
Yeni component'ler:
- CookieBanner (sağ alt banner + tam ekran ayarlar modal)
- 'Tümünü kabul', 'Tümünü reddet', 'Ayarları yönet'
- 4 kategori toggle: Zorunlu / Analitik / Reklam / Tercih
- Zorunlu kategori her zaman açık (KVKK)
- localStorage + cookie persistence (12 ay TTL)
- Versionlı (CONSENT_VERSION=1) — şema değişince yeniden sorma
- window.openCookieSettings() global helper (footer/policy sayfasından çağrılabilir)
- ConsentInit (Google Consent Mode v2 default deny)
- beforeInteractive Script ile gtag default deny yüklenir
- User onayladığında banner gtag('consent','update', ...) çağırır
- seo_settings.gtm_id doluysa GTM injection (asenkron)
- CookieSettingsButton (politika sayfasında 'ayarları değiştir')
Yeni sayfa:
- /cerez-politikasi — KVKK uyumlu çerez politikası metni
- 4 kategori detaylı açıklama + örnek çerez isimleri
- KVKK Madde 11 kapsamındaki kullanıcı hakları
- İletişim bilgileri site_settings'ten
Layout entegrasyonu:
- app/layout.tsx — ConsentInit head'e, CookieBanner body sonuna
- Footer'a 'Çerez Politikası' linki
Consent flag mapping (Consent Mode v2):
- Zorunlu → functionality_storage + security_storage (her zaman granted)
- Analitik → analytics_storage
- Reklam → ad_storage + ad_user_data + ad_personalization
- Tercih → personalization_storage
Önemli: Default state 'denied' — kullanıcı seçim yapmadan
hiçbir analytics/ads çerezi tetiklenmez. Google Ads Consent Mode v2 uyumlu.
31 route, public sayfalar static (1m revalidate).
This commit is contained in:
@@ -0,0 +1,138 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { SectionTitle } from "@/components/section-title";
|
||||||
|
import { CookieSettingsButton } from "@/components/cookie-settings-button";
|
||||||
|
import { getSiteSettings } from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
return buildMetadata("/cerez-politikasi", {
|
||||||
|
title: "Çerez Politikası",
|
||||||
|
description:
|
||||||
|
"Kovak Yazılım çerez politikası — KVKK ve GDPR uyumlu çerez kullanımı.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{
|
||||||
|
title: "Zorunlu çerezler",
|
||||||
|
description:
|
||||||
|
"Web sitesinin temel işlevleri için gereklidir. Oturum yönetimi, güvenlik, form gönderimi gibi temel işlemler bu çerezler olmadan çalışamaz. Devre dışı bırakılamaz.",
|
||||||
|
examples: ["kovak_session", "kovak_consent_v1"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Analitik çerezler",
|
||||||
|
description:
|
||||||
|
"Web sitesi ziyaretçilerinin sitenizi nasıl kullandığını anlamamıza yardımcı olur. Toplanan veriler anonimdir ve istatistiksel amaçlıdır.",
|
||||||
|
examples: ["_ga", "_gid", "_gat"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Reklam çerezleri",
|
||||||
|
description:
|
||||||
|
"Sizinle ilgili olabilecek reklamların gösterilmesi ve reklam kampanyalarının performansının ölçülmesi amacıyla kullanılır.",
|
||||||
|
examples: ["_gcl_au", "_fbp", "IDE", "ads/ga-audiences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tercih çerezleri",
|
||||||
|
description:
|
||||||
|
"Dil, bölge veya görünüm tercihlerinizi hatırlamak için kullanılır.",
|
||||||
|
examples: ["NID", "lang", "theme"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default async function CookiePolicyPage() {
|
||||||
|
const s = await getSiteSettings();
|
||||||
|
const email = s?.contact_email ?? siteConfig.contact.email;
|
||||||
|
const phone = s?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-3xl px-6 py-20">
|
||||||
|
<SectionTitle
|
||||||
|
align="left"
|
||||||
|
eyebrow="Yasal"
|
||||||
|
title="Çerez Politikası"
|
||||||
|
description={`Son güncelleme: ${new Date().toLocaleDateString("tr-TR")}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-wrap items-center gap-3 rounded-2xl border border-[var(--sky)]/30 bg-[var(--sky-50)] p-4">
|
||||||
|
<p className="flex-1 text-sm text-[var(--navy)]">
|
||||||
|
Çerez tercihlerinizi istediğiniz zaman değiştirebilirsiniz.
|
||||||
|
</p>
|
||||||
|
<CookieSettingsButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article className="prose prose-base mt-10 max-w-none text-[var(--foreground)]">
|
||||||
|
<h2>Çerez Nedir?</h2>
|
||||||
|
<p>
|
||||||
|
Çerezler (cookies), ziyaret ettiğiniz web siteleri tarafından
|
||||||
|
tarayıcınız aracılığıyla cihazınıza kaydedilen küçük metin
|
||||||
|
dosyalarıdır. Bu dosyalar sayesinde site, sizi sonraki ziyaretlerde
|
||||||
|
tanır ve deneyiminizi kişiselleştirir.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Hangi Çerezleri Kullanıyoruz?</h2>
|
||||||
|
<p>
|
||||||
|
Sitemizde 4 farklı kategoride çerez kullanılmaktadır. Zorunlu çerezler
|
||||||
|
dışındakileri çerez ayarları üzerinden devre dışı bırakabilirsiniz.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{CATEGORIES.map((c) => (
|
||||||
|
<section key={c.title} className="not-prose mt-6 rounded-xl border border-[var(--border)] bg-white p-5">
|
||||||
|
<h3 className="text-base font-semibold text-[var(--navy)]">
|
||||||
|
{c.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)]">
|
||||||
|
{c.description}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-xs text-[var(--muted)]">
|
||||||
|
<strong>Örnek çerezler:</strong>{" "}
|
||||||
|
{c.examples.map((e, i) => (
|
||||||
|
<span key={e}>
|
||||||
|
<code className="rounded bg-[var(--navy-50)] px-1.5 py-0.5 text-[var(--navy)]">
|
||||||
|
{e}
|
||||||
|
</code>
|
||||||
|
{i < c.examples.length - 1 && ", "}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<h2 className="mt-10">Çerezleri Nasıl Devre Dışı Bırakabilirim?</h2>
|
||||||
|
<p>
|
||||||
|
Tarayıcı ayarlarınızdan tüm çerezleri silebilir veya engelleyebilirsiniz.
|
||||||
|
Ancak zorunlu çerezler devre dışı bırakıldığında sitemizin bazı bölümleri
|
||||||
|
çalışmayabilir. Tercihlerinizi yönetmek için sayfanın altındaki "Çerez
|
||||||
|
ayarları" butonunu kullanabilirsiniz.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>KVKK Kapsamında Haklarınız</h2>
|
||||||
|
<p>
|
||||||
|
6698 sayılı Kişisel Verilerin Korunması Kanunu (KVKK) kapsamında
|
||||||
|
aşağıdaki haklara sahipsiniz:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Kişisel verilerinizin işlenip işlenmediğini öğrenme</li>
|
||||||
|
<li>İşlenmişse buna ilişkin bilgi talep etme</li>
|
||||||
|
<li>İşlenme amacını ve amacına uygun kullanılıp kullanılmadığını öğrenme</li>
|
||||||
|
<li>Eksik veya yanlış işlenmiş verilerin düzeltilmesini isteme</li>
|
||||||
|
<li>Silinmesini veya yok edilmesini isteme</li>
|
||||||
|
<li>Kanuna aykırı işleme sonucu zarara uğramışsanız tazminat talep etme</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>İletişim</h2>
|
||||||
|
<p>
|
||||||
|
Çerez politikamız veya kişisel verilerinizin işlenmesi hakkında soru
|
||||||
|
veya talepleriniz için bizimle iletişime geçebilirsiniz:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
E-posta: <a href={`mailto:${email}`}>{email}</a>
|
||||||
|
</li>
|
||||||
|
<li>Telefon: {phone}</li>
|
||||||
|
<li>Adres: {s?.contact_address ?? siteConfig.contact.address}</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-1
@@ -2,6 +2,9 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { ConsentInit } from "@/components/consent-init";
|
||||||
|
import { CookieBanner } from "@/components/cookie-banner";
|
||||||
|
import { getSeoSettings } from "@/lib/data";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -29,16 +32,28 @@ export const metadata: Metadata = {
|
|||||||
icons: { icon: "/logo.png" },
|
icons: { icon: "/logo.png" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
let gtmId: string | null = null;
|
||||||
|
try {
|
||||||
|
const seo = await getSeoSettings();
|
||||||
|
gtmId = seo?.gtm_id ?? null;
|
||||||
|
} catch {
|
||||||
|
gtmId = null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="tr"
|
lang="tr"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
|
<head>
|
||||||
|
<ConsentInit gtmId={gtmId} />
|
||||||
|
</head>
|
||||||
<body className="min-h-full flex flex-col bg-white text-[var(--foreground)]">
|
<body className="min-h-full flex flex-col bg-white text-[var(--foreground)]">
|
||||||
{children}
|
{children}
|
||||||
|
<CookieBanner />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Google Consent Mode v2 — defaults set to "denied" before any tag loads.
|
||||||
|
* After the user makes a choice, CookieBanner calls gtag('consent','update', ...).
|
||||||
|
*
|
||||||
|
* GTM/GA inject sadece site_settings.gtm_id doluysa yapılır.
|
||||||
|
*/
|
||||||
|
export function ConsentInit({ gtmId }: { gtmId?: string | null }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Script id="consent-default" strategy="beforeInteractive">
|
||||||
|
{`
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
window.gtag = gtag;
|
||||||
|
gtag('consent', 'default', {
|
||||||
|
ad_storage: 'denied',
|
||||||
|
ad_user_data: 'denied',
|
||||||
|
ad_personalization: 'denied',
|
||||||
|
analytics_storage: 'denied',
|
||||||
|
functionality_storage: 'granted',
|
||||||
|
personalization_storage: 'denied',
|
||||||
|
security_storage: 'granted',
|
||||||
|
wait_for_update: 500,
|
||||||
|
});
|
||||||
|
`}
|
||||||
|
</Script>
|
||||||
|
|
||||||
|
{gtmId && (
|
||||||
|
<>
|
||||||
|
<Script id="gtm-script" strategy="afterInteractive">
|
||||||
|
{`
|
||||||
|
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
|
||||||
|
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
|
||||||
|
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','${gtmId}');
|
||||||
|
`}
|
||||||
|
</Script>
|
||||||
|
<noscript>
|
||||||
|
<iframe
|
||||||
|
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
|
||||||
|
height="0"
|
||||||
|
width="0"
|
||||||
|
style={{ display: "none", visibility: "hidden" }}
|
||||||
|
/>
|
||||||
|
</noscript>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Cookie, Settings2, X } from "lucide-react";
|
||||||
|
import {
|
||||||
|
CONSENT_COOKIE_NAME,
|
||||||
|
CONSENT_STORAGE_KEY,
|
||||||
|
CONSENT_TTL_DAYS,
|
||||||
|
CONSENT_VERSION,
|
||||||
|
type ConsentState,
|
||||||
|
} from "@/lib/consent-types";
|
||||||
|
|
||||||
|
type Mode = "hidden" | "banner" | "settings";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
dataLayer?: any[];
|
||||||
|
gtag?: (...args: unknown[]) => void;
|
||||||
|
openCookieSettings?: () => void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(value: string) {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setDate(expires.getDate() + CONSENT_TTL_DAYS);
|
||||||
|
document.cookie = `${CONSENT_COOKIE_NAME}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConsent(): ConsentState | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = JSON.parse(raw) as ConsentState;
|
||||||
|
if (parsed.version !== CONSENT_VERSION) return null;
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConsent(state: ConsentState) {
|
||||||
|
localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(state));
|
||||||
|
setCookie(JSON.stringify({ a: state.analytics, m: state.marketing, p: state.preferences }));
|
||||||
|
|
||||||
|
// Google Consent Mode v2 update
|
||||||
|
if (typeof window !== "undefined" && window.gtag) {
|
||||||
|
window.gtag("consent", "update", {
|
||||||
|
ad_storage: state.marketing ? "granted" : "denied",
|
||||||
|
ad_user_data: state.marketing ? "granted" : "denied",
|
||||||
|
ad_personalization: state.marketing ? "granted" : "denied",
|
||||||
|
analytics_storage: state.analytics ? "granted" : "denied",
|
||||||
|
functionality_storage: "granted",
|
||||||
|
personalization_storage: state.preferences ? "granted" : "denied",
|
||||||
|
security_storage: "granted",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent("kovak:consent-updated", { detail: state }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CookieBanner() {
|
||||||
|
const [mode, setMode] = useState<Mode>("hidden");
|
||||||
|
const [analytics, setAnalytics] = useState(false);
|
||||||
|
const [marketing, setMarketing] = useState(false);
|
||||||
|
const [preferences, setPreferences] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const existing = loadConsent();
|
||||||
|
if (existing) {
|
||||||
|
setAnalytics(existing.analytics);
|
||||||
|
setMarketing(existing.marketing);
|
||||||
|
setPreferences(existing.preferences);
|
||||||
|
setMode("hidden");
|
||||||
|
} else {
|
||||||
|
// Tarayıcı yüklendikten sonra göster, FCP'yi etkilememek için kısa gecikme
|
||||||
|
const t = setTimeout(() => setMode("banner"), 800);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.openCookieSettings = () => setMode("settings");
|
||||||
|
return () => {
|
||||||
|
delete window.openCookieSettings;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const acceptAll = () => {
|
||||||
|
saveConsent({
|
||||||
|
necessary: true,
|
||||||
|
analytics: true,
|
||||||
|
marketing: true,
|
||||||
|
preferences: true,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
version: CONSENT_VERSION,
|
||||||
|
});
|
||||||
|
setAnalytics(true);
|
||||||
|
setMarketing(true);
|
||||||
|
setPreferences(true);
|
||||||
|
setMode("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectAll = () => {
|
||||||
|
saveConsent({
|
||||||
|
necessary: true,
|
||||||
|
analytics: false,
|
||||||
|
marketing: false,
|
||||||
|
preferences: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
version: CONSENT_VERSION,
|
||||||
|
});
|
||||||
|
setAnalytics(false);
|
||||||
|
setMarketing(false);
|
||||||
|
setPreferences(false);
|
||||||
|
setMode("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSelection = () => {
|
||||||
|
saveConsent({
|
||||||
|
necessary: true,
|
||||||
|
analytics,
|
||||||
|
marketing,
|
||||||
|
preferences,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
version: CONSENT_VERSION,
|
||||||
|
});
|
||||||
|
setMode("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === "hidden") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Banner */}
|
||||||
|
{mode === "banner" && (
|
||||||
|
<div className="fixed inset-x-3 bottom-3 z-50 sm:inset-x-auto sm:bottom-5 sm:left-5 sm:right-5 md:max-w-2xl">
|
||||||
|
<div className="rounded-2xl border border-[var(--border)] bg-white p-5 shadow-2xl shadow-[var(--navy)]/15 sm:p-6">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="hidden size-10 shrink-0 items-center justify-center rounded-xl bg-[var(--navy-50)] text-[var(--navy)] sm:flex">
|
||||||
|
<Cookie className="size-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-semibold text-[var(--navy)]">
|
||||||
|
Çerez tercihiniz
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-xs leading-relaxed text-[var(--muted)]">
|
||||||
|
Web sitemizde deneyiminizi geliştirmek, trafiği analiz etmek ve
|
||||||
|
reklam performansını ölçmek için çerezler kullanıyoruz.{" "}
|
||||||
|
<Link
|
||||||
|
href="/cerez-politikasi"
|
||||||
|
className="underline hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Çerez Politikası
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={acceptAll}
|
||||||
|
className="rounded-full bg-[var(--navy)] px-4 py-2 text-xs font-medium text-white transition hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Tümünü kabul et
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={rejectAll}
|
||||||
|
className="rounded-full border border-[var(--border)] bg-white px-4 py-2 text-xs font-medium text-[var(--navy)] transition hover:bg-[var(--navy-50)]"
|
||||||
|
>
|
||||||
|
Tümünü reddet
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMode("settings")}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-4 py-2 text-xs font-medium text-[var(--muted)] transition hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<Settings2 className="size-3.5" />
|
||||||
|
Ayarları yönet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Settings Modal */}
|
||||||
|
{mode === "settings" && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 p-4 sm:items-center"
|
||||||
|
onClick={() => setMode("banner")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-lg rounded-2xl bg-white shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between border-b border-[var(--border)] p-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-[var(--navy)]">
|
||||||
|
Çerez ayarları
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||||
|
Hangi çerez türlerine izin verdiğinizi seçin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Kapat"
|
||||||
|
onClick={() => setMode("banner")}
|
||||||
|
className="rounded-md p-1 text-[var(--muted)] hover:bg-[var(--navy-50)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<X className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[60vh] space-y-4 overflow-y-auto p-6">
|
||||||
|
<CategoryRow
|
||||||
|
title="Zorunlu çerezler"
|
||||||
|
description="Site temel işlevleri için gerekli. Oturum, güvenlik, form gönderimi. Devre dışı bırakılamaz."
|
||||||
|
checked
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<CategoryRow
|
||||||
|
title="Analitik çerezler"
|
||||||
|
description="Ziyaretçi sayısı, sayfa görüntüleme, trafik kaynağı gibi anonim istatistikler. Google Analytics."
|
||||||
|
checked={analytics}
|
||||||
|
onChange={setAnalytics}
|
||||||
|
/>
|
||||||
|
<CategoryRow
|
||||||
|
title="Reklam çerezleri"
|
||||||
|
description="Sizinle ilgili olabilecek reklamların gösterilmesi ve reklam performansının ölçülmesi. Google Ads, Meta Pixel."
|
||||||
|
checked={marketing}
|
||||||
|
onChange={setMarketing}
|
||||||
|
/>
|
||||||
|
<CategoryRow
|
||||||
|
title="Tercih çerezleri"
|
||||||
|
description="Dil, bölge, görüntüleme tercihlerinizi hatırlamak için."
|
||||||
|
checked={preferences}
|
||||||
|
onChange={setPreferences}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2 border-t border-[var(--border)] bg-[var(--navy-50)]/40 p-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={rejectAll}
|
||||||
|
className="rounded-full border border-[var(--border)] bg-white px-4 py-2 text-xs font-medium text-[var(--muted)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Tümünü reddet
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveSelection}
|
||||||
|
className="rounded-full border border-[var(--navy)] bg-white px-4 py-2 text-xs font-medium text-[var(--navy)] hover:bg-[var(--navy-50)]"
|
||||||
|
>
|
||||||
|
Seçimi kaydet
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={acceptAll}
|
||||||
|
className="rounded-full bg-[var(--navy)] px-4 py-2 text-xs font-medium text-white hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Tümünü kabul et
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategoryRow({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (v: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-[var(--border)] bg-white p-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-[var(--navy)]">{title}</p>
|
||||||
|
<p className="mt-1 text-xs leading-relaxed text-[var(--muted)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex shrink-0 cursor-pointer items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e) => onChange?.(e.target.checked)}
|
||||||
|
className="peer sr-only"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`h-6 w-11 rounded-full transition ${
|
||||||
|
disabled
|
||||||
|
? "bg-[var(--sky)]/60"
|
||||||
|
: "bg-[var(--border)] peer-checked:bg-[var(--navy)]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`absolute left-0.5 top-0.5 size-5 rounded-full bg-white shadow transition ${
|
||||||
|
checked ? "translate-x-5" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Settings2 } from "lucide-react";
|
||||||
|
|
||||||
|
export function CookieSettingsButton() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => window.openCookieSettings?.()}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full bg-[var(--navy)] px-4 py-2 text-xs font-medium text-white transition hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
<Settings2 className="size-3.5" />
|
||||||
|
Çerez ayarlarını değiştir
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -112,7 +112,13 @@ export async function Footer() {
|
|||||||
<p>
|
<p>
|
||||||
© {new Date().getFullYear()} {siteConfig.name}. Tüm hakları saklıdır.
|
© {new Date().getFullYear()} {siteConfig.name}. Tüm hakları saklıdır.
|
||||||
</p>
|
</p>
|
||||||
<p>Kocaeli, Türkiye</p>
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<Link href="/cerez-politikasi" className="hover:text-white">
|
||||||
|
Çerez Politikası
|
||||||
|
</Link>
|
||||||
|
<span>•</span>
|
||||||
|
<p>Kocaeli, Türkiye</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export type ConsentCategory =
|
||||||
|
| "necessary"
|
||||||
|
| "analytics"
|
||||||
|
| "marketing"
|
||||||
|
| "preferences";
|
||||||
|
|
||||||
|
export interface ConsentState {
|
||||||
|
necessary: true; // her zaman
|
||||||
|
analytics: boolean;
|
||||||
|
marketing: boolean;
|
||||||
|
preferences: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONSENT_VERSION = 1;
|
||||||
|
export const CONSENT_STORAGE_KEY = "kovak_consent_v1";
|
||||||
|
export const CONSENT_COOKIE_NAME = "kovak_consent";
|
||||||
|
export const CONSENT_TTL_DAYS = 365;
|
||||||
Reference in New Issue
Block a user