✨ feat(analytics): add PrivacyPolicy page
This commit is contained in:
+13
-4
@@ -4,10 +4,11 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
||||
import ErrorPage from 'components/pages/ErrorPage/index.js'
|
||||
import RootLayout from 'components/templates/RouteTracker/index.js'
|
||||
|
||||
const MainPage = React.lazy(() => import('components/pages/MainPage'))
|
||||
const ProjectsPage = React.lazy(() => import('components/pages/ProjectsPage'))
|
||||
const CurriculumPage = React.lazy(() => import('components/pages/CurriculumPage'))
|
||||
const ContactPage = React.lazy(() => import('components/pages/ContactPage'))
|
||||
const MainPage = React.lazy(() => import('components/pages/MainPage/index.js'))
|
||||
const ProjectsPage = React.lazy(() => import('components/pages/ProjectsPage/index.js'))
|
||||
const CurriculumPage = React.lazy(() => import('components/pages/CurriculumPage/index.js'))
|
||||
const ContactPage = React.lazy(() => import('components/pages/ContactPage/index.js'))
|
||||
const PrivacyPage = React.lazy(() => import('components/pages/PrivacyPage/index.js'))
|
||||
|
||||
const Loading = () => (
|
||||
<Backdrop open={true} aria-label="loading-screen">
|
||||
@@ -53,6 +54,14 @@ const router = createBrowserRouter([
|
||||
</React.Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'privacy',
|
||||
element: (
|
||||
<React.Suspense fallback={<Loading />}>
|
||||
<PrivacyPage />
|
||||
</React.Suspense>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { ListItem, Typography, ListItemProps } from '@mui/material'
|
||||
|
||||
interface ListItemTextProps extends ListItemProps {
|
||||
title?: string
|
||||
}
|
||||
const ListItemText: React.FC<ListItemTextProps> = ({ title, children, ...props }) => (
|
||||
<ListItem component={Typography} color="text.secondary" {...props}>
|
||||
{title && (
|
||||
<Typography component="b" fontWeight={700} color="text.primary">
|
||||
{title}{' '}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</ListItem>
|
||||
)
|
||||
|
||||
export default ListItemText
|
||||
@@ -0,0 +1,89 @@
|
||||
import { screen, fireEvent } 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 CookiePreferences from './index.js'
|
||||
|
||||
// Mock the analytics module
|
||||
const analytics = { plugins: { enable: vi.fn(), disable: vi.fn() } }
|
||||
vi.mock('use-analytics', () => ({
|
||||
useAnalytics: () => analytics,
|
||||
}))
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {}
|
||||
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('CookiePreferences', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorageMock.clear()
|
||||
})
|
||||
|
||||
it('accepts all cookies when clicking Accept All', () => {
|
||||
render(<CookiePreferences />)
|
||||
|
||||
expect(screen.getByText('Cookie Preferences')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Accept All'))
|
||||
fireEvent.click(screen.getByText('Save Preferences'))
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ necessary: true, analytics: true }))
|
||||
expect(analytics.plugins.disable).not.toHaveBeenCalled()
|
||||
expect(analytics.plugins.enable).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects analytics cookies when clicking Reject All', () => {
|
||||
render(<CookiePreferences />)
|
||||
|
||||
expect(screen.getByText('Cookie Preferences')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('Reject All'))
|
||||
fireEvent.click(screen.getByText('Save Preferences'))
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ necessary: true, analytics: false }))
|
||||
expect(analytics.plugins.enable).not.toHaveBeenCalled()
|
||||
expect(analytics.plugins.disable).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('toggles analytics cookies when clicking the Analytics button', () => {
|
||||
render(<CookiePreferences />)
|
||||
|
||||
expect(screen.getByText('Cookie Preferences')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByLabelText('analytics-cookies-toggle'))
|
||||
fireEvent.click(screen.getByText('Save Preferences'))
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ necessary: true, analytics: true }))
|
||||
expect(analytics.plugins.disable).not.toHaveBeenCalled()
|
||||
expect(analytics.plugins.enable).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders the cookie consent in the right state when consent in local storage', () => {
|
||||
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify({ necessary: true, analytics: true }))
|
||||
render(<CookiePreferences />)
|
||||
|
||||
expect(screen.getByText('Cookie Preferences')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByLabelText('analytics-cookies-toggle'))
|
||||
fireEvent.click(screen.getByText('Save Preferences'))
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith('m-tartari-cookie-consent', JSON.stringify({ necessary: true, analytics: false }))
|
||||
expect(analytics.plugins.enable).not.toHaveBeenCalled()
|
||||
expect(analytics.plugins.disable).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Box, Button, Link, Stack, Switch, Typography, SnackbarCloseReason, Snackbar, Alert } from '@mui/material'
|
||||
import { useAnalytics } from 'use-analytics'
|
||||
|
||||
import { ANALYTICS_PLUGINS, COOKIE_CONSENT_KEY } from 'components/utils/analytics/index.js'
|
||||
|
||||
interface CookieConsentState {
|
||||
necessary: boolean // Always true, but included for completeness
|
||||
analytics: boolean
|
||||
}
|
||||
|
||||
export type CookieConsentProps = {
|
||||
consentDisplayDefaultOpen?: boolean
|
||||
}
|
||||
|
||||
const CookieConsent: React.FC<CookieConsentProps> = ({ consentDisplayDefaultOpen = true }) => {
|
||||
const analytics = useAnalytics()
|
||||
const [consent, setConsent] = useState<CookieConsentState>({
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
})
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
const handleClose = (_event: React.SyntheticEvent | Event, reason?: SnackbarCloseReason) => {
|
||||
if (reason === 'clickaway') {
|
||||
return
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already given consent
|
||||
const savedConsent = localStorage.getItem(COOKIE_CONSENT_KEY)
|
||||
|
||||
if (savedConsent) {
|
||||
const parsedConsent = JSON.parse(savedConsent) as CookieConsentState
|
||||
setConsent(parsedConsent)
|
||||
}
|
||||
}, [analytics.plugins, consentDisplayDefaultOpen])
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
localStorage.setItem(COOKIE_CONSENT_KEY, JSON.stringify(consent))
|
||||
|
||||
// Initialize GA if analytics consent was given
|
||||
if (consent.analytics) {
|
||||
analytics.plugins.enable(ANALYTICS_PLUGINS)
|
||||
} else {
|
||||
analytics.plugins.disable(ANALYTICS_PLUGINS)
|
||||
}
|
||||
setOpen(true)
|
||||
}, [consent, analytics.plugins])
|
||||
|
||||
const handleChange = useCallback((type: keyof CookieConsentState) => {
|
||||
if (type === 'necessary') {
|
||||
return
|
||||
}
|
||||
|
||||
setConsent(prev => ({
|
||||
...prev,
|
||||
[type]: !prev[type],
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box id="cookie-preferences" aria-labelledby="cookie-preferences-title" aria-describedby="cookie-preferences-description">
|
||||
<Typography id="cookie-preferences-title" aria-label="cookie-preferences" variant="h3" component="h2" gutterBottom>
|
||||
Cookie Preferences
|
||||
</Typography>
|
||||
<Typography id="cookie-preferences-description" color="textSecondary" flexGrow={1}>
|
||||
Please set your preferences below. You can change these settings at any time in the future.
|
||||
<br />
|
||||
For more details, please see our{' '}
|
||||
<Link href="/privacy" color="textPrimary">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={theme => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
my: theme.spacing(2),
|
||||
p: theme.spacing(4),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
})}>
|
||||
<Stack direction="row" alignSelf="flex-end" mb={-2}>
|
||||
<Button onClick={() => setConsent({ necessary: true, analytics: false })} color="inherit" variant="text" sx={{ textTransform: 'none' }}>
|
||||
Reject All
|
||||
</Button>
|
||||
<Button onClick={() => setConsent({ necessary: true, analytics: true })} color="inherit" variant="text" sx={{ textTransform: 'none' }}>
|
||||
Accept All
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" alignContent="center">
|
||||
<Typography variant="body1" fontWeight="bold">
|
||||
Necessary Cookies
|
||||
</Typography>
|
||||
<Switch slotProps={{ input: { 'aria-label': 'necessary-cookies-toggle' } }} color="default" disabled checked />
|
||||
</Stack>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
These cookies are essential for the website to function properly and cannot be disabled.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" alignContent="center">
|
||||
<Typography variant="body1" fontWeight="bold">
|
||||
Analytics Cookies
|
||||
</Typography>
|
||||
<Switch
|
||||
checked={consent.analytics}
|
||||
slotProps={{ input: { 'aria-label': 'analytics-cookies-toggle' } }}
|
||||
onChange={() => handleChange('analytics')}
|
||||
color="default"
|
||||
/>
|
||||
</Stack>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
These cookies help us understand how visitors interact with our website, helping us improve our services.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Button onClick={handleSave} variant="outlined" sx={{ alignSelf: 'flex-end' }}>
|
||||
Save Preferences
|
||||
</Button>
|
||||
</Box>
|
||||
<Snackbar open={open} autoHideDuration={3000} onClose={handleClose}>
|
||||
<Alert onClose={handleClose} sx={{ backgroundColor: 'background.paper', width: '100%' }}>
|
||||
Preferences saved!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default CookieConsent
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
import PrivacyPolicy from './index.js'
|
||||
|
||||
describe('Privacy Policy', () => {
|
||||
it('renders the Privacy Policy page', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<PrivacyPolicy />
|
||||
</BrowserRouter>
|
||||
)
|
||||
expect(screen.getByText('Privacy Policy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Google Analytics')).toBeInTheDocument()
|
||||
expect(screen.getByText('Your Rights')).toBeInTheDocument()
|
||||
expect(screen.getByText('Contact Us')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import { Typography, Link, List as MuiList, styled, Box } from '@mui/material'
|
||||
|
||||
import ListItemText from 'components/atoms/ListItemText/index.js'
|
||||
|
||||
const List = styled(MuiList)(({ theme }) => ({
|
||||
listStyleType: 'disc',
|
||||
paddingLeft: theme.spacing(4),
|
||||
'& .MuiListItem-root': {
|
||||
display: 'list-item',
|
||||
padding: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
const PrivacyPolicy: React.FC = () => {
|
||||
return (
|
||||
<Box aria-labelledby="privacy-policy">
|
||||
<Typography id="privacy-policy" aria-label="privacy-policy" variant="h3" component="h2" gutterBottom>
|
||||
Privacy Policy
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="textSecondary" component="p" pb={4}>
|
||||
Last updated: May 27, 2025
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
Introduction
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
m-tartari ("we", "our", or "us") respects your privacy and is committed to protecting your personal data. This privacy policy will inform you
|
||||
about how we look after your personal data when you visit our website and tell you about your privacy rights and how the law protects you.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mt: 4 }}>
|
||||
The Data We Collect About You
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
We collect and process the following information when you visit our website:
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItemText title="Technical Data">
|
||||
includes internet protocol (IP) address, browser type and version, time zone setting and location, browser plug-in types and versions,
|
||||
operating system and platform, and other technology on the devices you use to access this website.
|
||||
</ListItemText>
|
||||
<ListItemText title="Usage Data"> includes information about how you use our website, products, and services.</ListItemText>
|
||||
</List>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mt: 4 }}>
|
||||
Cookies
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
We use cookies to distinguish you from other users of our website. This helps us to provide you with a good experience when you browse our
|
||||
website and also allows us to improve our site.
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
We use the following types of cookies:
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItemText title="Necessary Cookies:">
|
||||
These are essential to enable you to use the website and its features. Without these cookies, services you have asked for cannot be
|
||||
provided.
|
||||
</ListItemText>
|
||||
<ListItemText title="Analytics Cookies:">
|
||||
We use Google Analytics to help us understand how visitors interact with our website. These cookies collect information about your visit to
|
||||
our website, the content you viewed, the links you followed, and information about your browser, device, and IP address. We do this to
|
||||
improve how our website works.
|
||||
</ListItemText>
|
||||
</List>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mt: 4 }}>
|
||||
Google Analytics
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
This website uses Google Analytics 4, a web analytics service provided by Google, Inc. ("Google"). Google Analytics uses cookies to help the
|
||||
website analyze how users use the site. The information generated by the cookie about your use of the website will be transmitted to and
|
||||
stored by Google on servers in the United States.
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
We have implemented the following measures to be GDPR compliant:
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItemText>We have obtained your consent before processing your data through cookies</ListItemText>
|
||||
<ListItemText>We have implemented IP address anonymization</ListItemText>
|
||||
<ListItemText>We have disabled data sharing with Google</ListItemText>
|
||||
<ListItemText>We have set the data retention period to the minimum required for our analysis</ListItemText>
|
||||
</List>
|
||||
<Typography component="p" color="textSecondary">
|
||||
You can prevent the collection of your data by Google Analytics by declining analytics cookies in our cookie consent banner or by using the
|
||||
Google Analytics Opt-out Browser Add-on available at{' '}
|
||||
<Link color="textPrimary" href="https://tools.google.com/dlpage/gaoptout" target="_blank" rel="noopener noreferrer">
|
||||
https://tools.google.com/dlpage/gaoptout
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mt: 4 }}>
|
||||
Your Rights
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
Under the General Data Protection Regulation (GDPR), you have various rights in relation to your personal data, including:
|
||||
</Typography>
|
||||
|
||||
<List dense>
|
||||
<ListItemText>The right to access your personal data</ListItemText>
|
||||
<ListItemText>The right to rectification of your personal data</ListItemText>
|
||||
<ListItemText>The right to erasure of your personal data</ListItemText>
|
||||
<ListItemText>The right to restrict processing of your personal data</ListItemText>
|
||||
<ListItemText>The right to data portability</ListItemText>
|
||||
<ListItemText>The right to object to processing of your personal data</ListItemText>
|
||||
</List>
|
||||
|
||||
<Typography variant="h5" component="h2" gutterBottom sx={{ mt: 4 }}>
|
||||
Contact Us
|
||||
</Typography>
|
||||
<Typography component="p" color="textSecondary">
|
||||
If you have any questions about this privacy policy or our privacy practices, please contact us at{' '}
|
||||
<Link color="textPrimary" href="mailto:privacy@m-tartari.eu" target="_blank" rel="noopener noreferrer">
|
||||
privacy@m-tartari.eu
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivacyPolicy
|
||||
@@ -0,0 +1,24 @@
|
||||
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 PrivacyPage from './index.js'
|
||||
|
||||
// Mock the analytics module
|
||||
vi.mock('use-analytics', () => ({
|
||||
useAnalytics: () => ({ plugins: { enable: vi.fn(), disable: vi.fn() } }),
|
||||
}))
|
||||
|
||||
describe('PrivacyPage', () => {
|
||||
it('renders the Privacy Policy page', () => {
|
||||
render(<PrivacyPage />)
|
||||
expect(screen.getAllByText('Privacy Policy')).toHaveLength(3) // title, body (cookie preferences section) and footer
|
||||
expect(screen.getByText('Google Analytics')).toBeInTheDocument()
|
||||
expect(screen.getByText('Your Rights')).toBeInTheDocument()
|
||||
expect(screen.getByText('Contact Us')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('Cookie Preferences')).toHaveLength(2) // body and footer
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import Page from 'components/templates/Page/index.js'
|
||||
import CookiePreferences from 'components/molecules/CookiePreferences/index.js'
|
||||
import PrivacyPolicy from 'components/molecules/PrivacyPolicy/index.js'
|
||||
import { Divider } from '@mui/material'
|
||||
|
||||
const PrivacyPage: React.FC = () => {
|
||||
return (
|
||||
<Page title="Privacy" showCookieConsent={false}>
|
||||
<PrivacyPolicy />
|
||||
<Divider sx={{ my: 4 }} />
|
||||
<CookiePreferences />
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivacyPage
|
||||
Reference in New Issue
Block a user