Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/api/track/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import { supabaseAdmin } from '@/lib/supabase';
export async function POST(request: Request) {
try {
const { event_type, payload } = await request.json();
const consent = request.headers.get('x-cookie-consent');

// Final check for consent of cookies
let finalPayload = { ...payload };
if (consent !== 'accepted') {
finalPayload.userAgent = 'Anonymized';
}
// The "Store" phase: Writing to Supabase
const { data, error } = await supabaseAdmin
.from('tracking_events')
.insert([{
event_type,
payload,
payload: finalPayload,
sync_status: 'PENDING' // Initial status for the Forwarder
}]);

Expand Down
6 changes: 5 additions & 1 deletion app/hooks/TrackPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { getConsentCookie } from "@/components/CookieBanner";
import { useEffect } from "react";

type TrackPageProps = {
Expand All @@ -8,6 +9,9 @@ type TrackPageProps = {

export default function useTrackPage({ page }: TrackPageProps) {
useEffect(() => {
const consent = getConsentCookie();
if (consent === null) return;

const trackPage = async () => {
try {
await fetch("/api/track", {
Expand All @@ -20,7 +24,7 @@ export default function useTrackPage({ page }: TrackPageProps) {
payload: {
page,
url: window.location.href,
userAgent: navigator.userAgent,
userAgent: consent === 'accepted' ? navigator.userAgent : 'Anonymized',
},
}),
});
Expand Down
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from 'next';
import localFont from 'next/font/local';
import Image from 'next/image';
import NavBar from '@/components/NavBar';
import CookieBanner from '@/components/CookieBanner';

import './globals.css';

Expand Down Expand Up @@ -33,6 +34,7 @@ export default function RootLayout({
>
<NavBar />
{children}
<CookieBanner />
</body>
</html>
);
Expand Down
81 changes: 81 additions & 0 deletions components/CookieBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';

const CONSENT_COOKIE_NAME = 'runway_cookie_consent';
const COOKIE_MAX_AGE_DAYS = 365;

export function getConsentCookie(): string | null {
if (typeof document === 'undefined') return null;
const match = document.cookie.match(
new RegExp('(^| )' + CONSENT_COOKIE_NAME + '=([^;]+)')
);
return match ? match[2] : null;
}

function setConsentCookie(value: 'accepted' | 'rejected') {
const maxAge = COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${maxAge}; SameSite=Lax`;
}

export default function CookieBanner() {
const router = useRouter();
const [mounted, setMounted] = useState(false);
const [showBanner, setShowBanner] = useState(false);

useEffect(() => {
setMounted(true);
const consent = getConsentCookie();
setShowBanner(consent === null);
}, []);

const handleAccept = () => {
setConsentCookie('accepted');
setShowBanner(false);
router.refresh(); // Tells Next.js to re-run server logic/middleware
};

const handleOptOut = () => {
setConsentCookie('rejected');
setShowBanner(false);
router.refresh(); // Updates the 'x-cookie-consent' header globally
};

// If we haven't mounted yet, return null to prevent hydration flicker
if (!mounted || !showBanner) return null;

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-label="Cookie consent"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
aria-hidden="true"
/>
{/* Modal window */}
<div className="relative w-full max-w-lg rounded-xl border bg-background p-6 shadow-2xl sm:p-8">
<h2 className="mb-3 text-lg font-semibold text-foreground sm:text-xl">
Cookie notice
</h2>
<p className="mb-6 text-sm text-muted-foreground sm:text-base">
We use cookies to improve your experience and for analytics.
You can accept or opt out.
</p>
<div className="flex flex-wrap items-center gap-3">
<Button onClick={handleAccept} size="default" className="min-w-[100px]">
Accept
</Button>
<Button onClick={handleOptOut} variant="outline" size="default" className="min-w-[100px]">
Opt out
</Button>
</div>
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server';

const CONSENT_COOKIE_NAME = 'runway_cookie_consent';

export function middleware(request: NextRequest) {
const consent = request.cookies.get(CONSENT_COOKIE_NAME)?.value ?? null;
const response = NextResponse.next();
if (consent === 'accepted' || consent === 'rejected') {
response.headers.set('x-cookie-consent', consent);
}else {
response.headers.set('x-cookie-consent', 'undecided');
}
return response;

}

export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};