Skip to content

feat(ui): add GDPR-compliant cookie consent banner with i18n support#113

Merged
ViktorSvertoka merged 2 commits into
developfrom
feature/cookie-consent-banner
Jan 6, 2026
Merged

feat(ui): add GDPR-compliant cookie consent banner with i18n support#113
ViktorSvertoka merged 2 commits into
developfrom
feature/cookie-consent-banner

Conversation

@AlinaRyabova
Copy link
Copy Markdown
Collaborator

@AlinaRyabova AlinaRyabova commented Jan 6, 2026

Summary by CodeRabbit

  • New Features
    • Added a dismissible cookie consent banner with accept/decline actions and a close option for small screens; user choice is persisted for future visits. Banner includes a privacy policy link and appears subtly in the site layout.
    • Localization added for English, Polish, and Ukrainian.

✏️ Tip: You can customize this high-level summary in your review settings.

@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 6, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit bd51862
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/695d58e91028aa0009571d2c
😎 Deploy Preview https://deploy-preview-113--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 6, 2026

📝 Walkthrough

Walkthrough

A client-side CookieBanner component was added and mounted in the app layout; it shows a localized, dismissible cookie consent banner that persists accept/decline in localStorage. Localization keys for the banner were added to English, Polish, and Ukrainian message files.

Changes

Cohort / File(s) Summary
Layout Integration
frontend/app/[locale]/layout.tsx
Imported and rendered <CookieBanner /> inside the LocaleLayout near Footer and Toaster.
Cookie Banner Component
frontend/components/shared/CookieBanner.tsx
New client React (client) component: mounts, checks localStorage for consent, shows after 500ms if none, provides accept/decline actions that persist choice to localStorage and hide banner, uses i18n, responsive layout, includes privacy link and inline close for small screens.
Localization Strings
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added top-level CookieBanner keys: title, description, policyLink, accept, decline in each language file (additive only).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Browser
  participant LocaleLayout
  participant CookieBanner
  participant LocalStorage
  Note over CookieBanner,LocalStorage `#F7F7F7`: Mount & consent check
  Browser->>LocaleLayout: Load page (with locale)
  LocaleLayout->>CookieBanner: Render component
  CookieBanner->>LocalStorage: read("cookie_consent")
  alt consent exists
    LocalStorage-->>CookieBanner: "accepted" / "declined"
    CookieBanner-->>Browser: do not display banner
  else no consent
    CookieBanner-->>Browser: show banner (after 500ms)
    Browser->>CookieBanner: user clicks Accept
    CookieBanner->>LocalStorage: write("cookie_consent","accepted")
    CookieBanner-->>Browser: hide banner
    Note right of CookieBanner: Accept/Decline symmetric flow (writes value and hides)
  end
  Note over Browser,CookieBanner `#EEF7FF`: Privacy link opens route (Link)
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 A banner pops where footfalls tread,
Asking kindly about cookies spread,
In three tongues bright, consent to keep,
Stored in quiet, safe and deep. 🍪

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a GDPR-compliant cookie consent banner with internationalization support, which is reflected in all modified files.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1d5024 and bd51862.

📒 Files selected for processing (1)
  • frontend/components/shared/CookieBanner.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/components/shared/CookieBanner.tsx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI Agents
In @frontend/components/shared/CookieBanner.tsx:
- Around line 23-31: The CookieBanner currently writes consent to localStorage
via handleAccept and handleDecline but no tracking reacts to that; either remove
the CookieBanner component entirely (and delete handleAccept/handleDecline and
any imports) or implement consent-driven analytics: add a single initializer
function (e.g., initializeAnalytics or startAnalytics) that imports/initializes
@vercel/analytics and only call it when handleAccept runs, ensure app startup
checks localStorage['cookie-consent'] before calling initializeAnalytics, and
implement a revokeConsent function that clears stored consent and calls a
stopAnalytics/cleanup routine to disable tracking; update CookieBanner to
reflect these flows and ensure no analytics module is loaded/initialized until
consent === 'accepted'.
🧹 Nitpick comments (2)
frontend/components/shared/CookieBanner.tsx (2)

14-21: Document the rationale for the 500ms delay.

The 500ms delay before showing the banner appears arbitrary. Consider documenting why this delay exists (e.g., to avoid immediately interrupting the user experience) or making it configurable.

💡 Suggested improvement
  useEffect(() => {
    setIsMounted(true);
    const consent = localStorage.getItem('cookie-consent');
    if (!consent) {
+     // Delay showing the banner to avoid immediately interrupting the user
      const timer = setTimeout(() => setIsVisible(true), 500);
      return () => clearTimeout(timer);
    }
  }, []);

23-31: Add error handling for localStorage unavailability.

The code assumes localStorage is always available, but it may be disabled in private browsing mode or blocked by browser settings. This would cause the handlers to throw an error and break the component.

🔧 Proposed fix
  const handleAccept = () => {
-   localStorage.setItem('cookie-consent', 'accepted');
+   try {
+     localStorage.setItem('cookie-consent', 'accepted');
+   } catch (error) {
+     console.error('Failed to save cookie consent:', error);
+   }
    setIsVisible(false);
  };

  const handleDecline = () => {
-   localStorage.setItem('cookie-consent', 'declined');
+   try {
+     localStorage.setItem('cookie-consent', 'declined');
+   } catch (error) {
+     console.error('Failed to save cookie consent:', error);
+   }
    setIsVisible(false);
  };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5bf2606 and b1d5024.

📒 Files selected for processing (5)
  • frontend/app/[locale]/layout.tsx
  • frontend/components/shared/CookieBanner.tsx
  • frontend/messages/en.json
  • frontend/messages/pl.json
  • frontend/messages/uk.json
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/[locale]/layout.tsx (1)
frontend/components/shared/CookieBanner.tsx (1)
  • CookieBanner (9-83)
🔇 Additional comments (9)
frontend/messages/uk.json (1)

144-151: LGTM! Ukrainian translations are complete and well-structured.

The CookieBanner translation keys are properly structured and the Ukrainian text is clear and appropriate for a cookie consent banner.

frontend/app/[locale]/layout.tsx (2)

50-50: Proper placement and integration of the cookie banner.

The CookieBanner is correctly positioned after the Footer and Toaster, inside the ThemeProvider, ensuring it appears consistently across all locales and has access to theme context.


15-15: No issues found. Next.js version 16.0.7 is a legitimate security-patched release in the 16.x line, with 16.1.1 being the latest stable version. The version specification in package.json is valid and requires no changes.

frontend/messages/en.json (1)

144-151: LGTM! English translations are clear and professional.

The CookieBanner translations follow proper structure and use appropriate language for GDPR consent. The "Accept Cookies" text is more explicit than just "Accept" which improves clarity.

frontend/messages/pl.json (1)

144-151: LGTM! Polish translations are complete and well-structured.

The CookieBanner translation keys are properly structured and the Polish text is appropriate for the cookie consent banner.

frontend/components/shared/CookieBanner.tsx (4)

73-79: Good accessibility: Close button includes proper ARIA label.

The mobile close button includes an appropriate aria-label for screen readers, which improves accessibility for users who rely on assistive technologies.


44-49: No action needed – privacy policy route exists.

The privacy-policy route is properly configured at frontend/app/[locale]/privacy-policy/page.tsx, so the link will resolve correctly without a 404 error.


63-70: No action needed—the Button component fully supports the "primary" variant.

The ButtonProps interface explicitly defines variant?: 'primary' | 'secondary' | 'outline', and the "primary" variant has complete styling implementation. The usage in CookieBanner.tsx is correct.

Likely an incorrect or invalid review comment.


36-36: No changes needed; z-index value is appropriately positioned.

The z-index of 100 sits correctly within the application's layer hierarchy: above navigation elements (z-50) and the header, but below critical modals (z-9999). No conflicts exist with other fixed/absolute positioned elements. While there's no formal design system z-index scale, the implicit scale demonstrates intentional layering where 100 is appropriate for a persistent cookie banner.

Comment on lines +9 to +83
export function CookieBanner() {
const t = useTranslations('CookieBanner');
const [isVisible, setIsVisible] = useState(false);
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
setIsMounted(true);
const consent = localStorage.getItem('cookie-consent');
if (!consent) {
const timer = setTimeout(() => setIsVisible(true), 500);
return () => clearTimeout(timer);
}
}, []);

const handleAccept = () => {
localStorage.setItem('cookie-consent', 'accepted');
setIsVisible(false);
};

const handleDecline = () => {
localStorage.setItem('cookie-consent', 'declined');
setIsVisible(false);
};

if (!isMounted || !isVisible) return null;

return (
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6 animate-in slide-in-from-bottom-full fade-in duration-700">
<div className="mx-auto max-w-4xl rounded-2xl border border-gray-200 bg-white/90 p-5 shadow-2xl backdrop-blur-md dark:border-gray-800 dark:bg-gray-900/90 md:flex md:items-center md:justify-between md:gap-6">
<div className="mb-4 md:mb-0 md:flex-1">
<h3 className="mb-1 text-lg font-semibold text-gray-900 dark:text-white">
{t('title')}
</h3>
<p className="text-sm leading-relaxed text-gray-600 dark:text-gray-300">
{t('description')}{' '}
<Link
href="/privacy-policy"
className="font-medium text-blue-600 underline decoration-blue-600/30 underline-offset-4 transition-colors hover:text-blue-700 hover:decoration-blue-700 dark:text-blue-500 dark:hover:text-blue-400"
>
{t('policyLink')}
</Link>
.
</p>
</div>

<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Button
variant="outline"
size="sm"
onClick={handleDecline}
className="w-full sm:w-auto"
>
{t('decline')}
</Button>
<Button
variant="primary"
size="sm"
onClick={handleAccept}
className="w-full sm:w-auto shadow-lg shadow-blue-500/20"
>
{t('accept')}
</Button>
</div>

<button
onClick={handleDecline}
className="absolute right-2 top-2 rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300 md:hidden"
aria-label={t('decline')}
>
<X size={20} />
</button>
</div>
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing: Consent revocation mechanism.

Once a user makes a choice, there's no way for them to change their mind or revoke consent. GDPR requires that users can withdraw consent as easily as they can give it. Consider adding a mechanism (e.g., a footer link or settings page) where users can clear their consent and see the banner again.

Would you like me to suggest an implementation for a consent revocation mechanism, such as a "Cookie Settings" link in the footer that clears the localStorage value?

Comment on lines +23 to +31
const handleAccept = () => {
localStorage.setItem('cookie-consent', 'accepted');
setIsVisible(false);
};

const handleDecline = () => {
localStorage.setItem('cookie-consent', 'declined');
setIsVisible(false);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for cookie consent checks and tracking script integration
rg -n -C3 "cookie-consent" --type=ts --type=tsx --type=js --type=jsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

#!/bin/bash
rg -n -C3 "(gtag|analytics|tracking)" --type=ts --type=tsx --type=js --type=jsx -g '!node_modules'

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

#!/bin/bash
# Also search for common patterns that might indicate consent enforcement
rg -n "cookie|consent|GDPR|tracking" frontend/components/shared/CookieBanner.tsx -C5

Repository: DevLoversTeam/devlovers.net

Length of output: 776


🏁 Script executed:

#!/bin/bash
# Check if there's an env file or config that might load tracking conditionally
fd -e "env" -e "config" -e "next.config" -e "vercel.json" . 2>/dev/null | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Search for cookie-consent usage without file type flags
rg -n -C3 "cookie-consent"

Repository: DevLoversTeam/devlovers.net

Length of output: 1442


🏁 Script executed:

#!/bin/bash
# Search for tracking and analytics patterns
rg -n "gtag|analytics|tracking|pixel" --max-count=20

Repository: DevLoversTeam/devlovers.net

Length of output: 20569


🏁 Script executed:

#!/bin/bash
# Look for conditional script loading or consent checks
rg -n "document\.head|Script|tracking|gtag" . | head -40

Repository: DevLoversTeam/devlovers.net

Length of output: 6455


🏁 Script executed:

#!/bin/bash
# Check for Next.js Script component usage
rg -n "next/script|<Script" . | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Look at the full CookieBanner component structure
cat -n frontend/components/shared/CookieBanner.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 3506


🏁 Script executed:

#!/bin/bash
# Search for @vercel/analytics usage
rg -n "@vercel/analytics|Analytics" . --max-count=30

Repository: DevLoversTeam/devlovers.net

Length of output: 399


🏁 Script executed:

#!/bin/bash
# Check the main app layout/root files
find frontend/app -name "layout.*" -o -name "page.*" | head -10 | xargs -I {} sh -c 'echo "=== {} ===" && head -50 {}'

Repository: DevLoversTeam/devlovers.net

Length of output: 8649


🏁 Script executed:

#!/bin/bash
# Look for any script initialization in root app files
rg -n "import.*Analytics|useEffect.*tracking|gtag" frontend/app --max-count=20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Check if there's any conditional loading of scripts based on consent
rg -n "cookie-consent.*Analytics|Analytics.*cookie|consent.*gtag" . --max-count=20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Find and check the root layout file
find frontend/app -maxdepth 2 -name "layout.tsx" | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && cat -n {}'

Repository: DevLoversTeam/devlovers.net

Length of output: 3440


🏁 Script executed:

#!/bin/bash
# Check for any configuration that might initialize analytics
rg -n "instrument|initialize|setup" frontend/app --type=ts --type=tsx 2>/dev/null | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

#!/bin/bash
# Double-check that @vercel/analytics is truly not imported anywhere
rg -r "from.*@vercel/analytics|import.*@vercel/analytics" frontend/

Repository: DevLoversTeam/devlovers.net

Length of output: 326


Remove or repurpose the CookieBanner—currently it stores consent with no tracking system to enforce it against.

The component saves user consent preferences to localStorage, but the codebase has no analytics or tracking systems integrated. The @vercel/analytics package is installed but unused. Users are prompted to accept/decline cookies, but this preference has no effect since there are no cookies or trackers to control.

Either remove the banner if no analytics are planned, or implement a complete consent-driven tracking solution where:

  1. Analytics/tracking scripts only load after user accepts consent
  2. The stored preference is actively checked before initializing any tracking
  3. Users can revoke consent and stop tracking

As-is, the banner creates a compliance-adjacent appearance without actual protection, which is worse than no banner.

🤖 Prompt for AI Agents
In @frontend/components/shared/CookieBanner.tsx around lines 23 - 31, The
CookieBanner currently writes consent to localStorage via handleAccept and
handleDecline but no tracking reacts to that; either remove the CookieBanner
component entirely (and delete handleAccept/handleDecline and any imports) or
implement consent-driven analytics: add a single initializer function (e.g.,
initializeAnalytics or startAnalytics) that imports/initializes
@vercel/analytics and only call it when handleAccept runs, ensure app startup
checks localStorage['cookie-consent'] before calling initializeAnalytics, and
implement a revokeConsent function that clears stored consent and calls a
stopAnalytics/cleanup routine to disable tracking; update CookieBanner to
reflect these flows and ensure no analytics module is loaded/initialized until
consent === 'accepted'.

@ViktorSvertoka ViktorSvertoka merged commit fe5ae23 into develop Jan 6, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the feature/cookie-consent-banner branch January 6, 2026 19:02
liudmylasovetovs pushed a commit that referenced this pull request Jan 9, 2026
feat(ui): add GDPR-compliant cookie consent banner with i18n support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants