feat(ui): add GDPR-compliant cookie consent banner with i18n support#113
Conversation
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughA 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
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)
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
frontend/app/[locale]/layout.tsxfrontend/components/shared/CookieBanner.tsxfrontend/messages/en.jsonfrontend/messages/pl.jsonfrontend/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-labelfor 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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
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?
| const handleAccept = () => { | ||
| localStorage.setItem('cookie-consent', 'accepted'); | ||
| setIsVisible(false); | ||
| }; | ||
|
|
||
| const handleDecline = () => { | ||
| localStorage.setItem('cookie-consent', 'declined'); | ||
| setIsVisible(false); | ||
| }; |
There was a problem hiding this comment.
🧩 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=jsxRepository: 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 -C5Repository: 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 -20Repository: 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=20Repository: 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 -40Repository: DevLoversTeam/devlovers.net
Length of output: 6455
🏁 Script executed:
#!/bin/bash
# Check for Next.js Script component usage
rg -n "next/script|<Script" . | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
#!/bin/bash
# Look at the full CookieBanner component structure
cat -n frontend/components/shared/CookieBanner.tsxRepository: DevLoversTeam/devlovers.net
Length of output: 3506
🏁 Script executed:
#!/bin/bash
# Search for @vercel/analytics usage
rg -n "@vercel/analytics|Analytics" . --max-count=30Repository: 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=20Repository: 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=20Repository: 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 -20Repository: 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:
- Analytics/tracking scripts only load after user accepts consent
- The stored preference is actively checked before initializing any tracking
- 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'.
feat(ui): add GDPR-compliant cookie consent banner with i18n support
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.