diff --git a/src/components/molecules/CookieConsent/CookieConsent.test.tsx b/src/components/molecules/CookieConsent/CookieConsent.test.tsx new file mode 100644 index 0000000..42e707c --- /dev/null +++ b/src/components/molecules/CookieConsent/CookieConsent.test.tsx @@ -0,0 +1,64 @@ +import { render, screen, fireEvent } from '@testing-library/react' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { useAnalytics } from 'use-analytics' + +import CookieConsent from './index.js' + +const analytics = { plugins: { enable: vi.fn(), disable: vi.fn() } } + +// Mock the analytics module +vi.mock('use-analytics', () => ({ + useAnalytics: () => analytics, +})) + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: vi.fn((key: string) => store[key] || null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value + }), + clear: vi.fn(() => { + store = {} + }), + } +})() + +Object.defineProperty(window, 'localStorage', { value: localStorageMock }) + +describe('CookieConsent', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorageMock.clear() + }) + + it('renders the cookie consent dialog when no consent is stored', () => { + render() + + expect(screen.getByText('Cookie Preferences')).toBeInTheDocument() + expect(screen.getByText(/We use cookies to improve your browsing experience/)).toBeInTheDocument() + expect(screen.getByText('Accept All')).toBeInTheDocument() + expect(screen.getByText('Reject All')).toBeInTheDocument() + }) + + it('accepts all cookies when clicking Accept All', () => { + render() + + fireEvent.click(screen.getByText('Accept All')) + + expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ analytics: true, necessary: true })) + expect(analytics.plugins.disable).not.toHaveBeenCalled() + expect(analytics.plugins.enable).toHaveBeenCalled() + }) + + it('rejects analytics cookies when clicking Reject All', () => { + render() + + fireEvent.click(screen.getByText('Reject All')) + + expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ analytics: false, necessary: true })) + expect(analytics.plugins.enable).not.toHaveBeenCalled() + expect(analytics.plugins.disable).toHaveBeenCalled() + }) +}) diff --git a/src/components/molecules/CookieConsent/index.tsx b/src/components/molecules/CookieConsent/index.tsx new file mode 100644 index 0000000..1b598ab --- /dev/null +++ b/src/components/molecules/CookieConsent/index.tsx @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback } from 'react' +import { Box, Button, Link, Stack, Typography, Unstable_TrapFocus as TrapFocus, Paper, Fade } from '@mui/material' +import { useAnalytics } from 'use-analytics' + +import { ANALYTICS_PLUGINS, COOKIE_CONSENT_KEY } from 'components/utils/analytics/index.js' + +interface Consent { + necessary: boolean // Always true, but included for completeness + analytics: boolean +} + +const CookieConsent = () => { + const [open, setOpen] = useState(false) + const analytics = useAnalytics() + + useEffect(() => { + // Check if user has already given consent + const savedConsent = localStorage.getItem(COOKIE_CONSENT_KEY) + + if (!savedConsent) { + setOpen(true) + } else { + const parsedConsent = JSON.parse(savedConsent) as Consent + if (parsedConsent.analytics) { + analytics.plugins.enable(ANALYTICS_PLUGINS) + } + } + }, [analytics]) + + const handleAcceptAll = useCallback(() => { + localStorage.setItem(COOKIE_CONSENT_KEY, JSON.stringify({ analytics: true, necessary: true })) + analytics.plugins.enable(ANALYTICS_PLUGINS) + setOpen(false) + }, [analytics.plugins]) + + const handleRejectAll = useCallback(() => { + localStorage.setItem(COOKIE_CONSENT_KEY, JSON.stringify({ analytics: false, necessary: true })) + analytics.plugins.disable(ANALYTICS_PLUGINS) + setOpen(false) + }, [analytics.plugins]) + + return ( + <> + + + ({ + position: 'fixed', + bottom: 0, + left: '50%', + transform: 'translate(-50%, 0)', + width: theme.breakpoints.values.sm, + maxWidth: `calc(100% - ${theme.spacing(2)})`, + mb: theme.spacing(1), + p: theme.spacing(3), + zIndex: 9999, + })}> + + + Cookie Preferences + + + We use cookies to improve your browsing experience and analyze our traffic. + By clicking "Accept All", you consent to our use of cookies. + + For more details, please see our{' '} + + Privacy Policy + + . + + + + + + + Customize + + + + + + + + + ) +} + +export default CookieConsent diff --git a/src/components/molecules/Footer/Footer.test.tsx b/src/components/molecules/Footer/Footer.test.tsx new file mode 100644 index 0000000..ac1656a --- /dev/null +++ b/src/components/molecules/Footer/Footer.test.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { screen } from '@testing-library/react' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { useAnalytics } from 'use-analytics' + +import { renderWithRouter as render } from 'components/utils/tests/index.js' + +import Footer from './index.js' + +// Mock the analytics module +const analytics = { plugins: { enable: vi.fn(), disable: vi.fn() } } +vi.mock('use-analytics', () => ({ + useAnalytics: () => analytics, +})) + +describe('Footer', () => { + it('renders the Footer', async () => { + render(