Cookie Consent
This commit is contained in:
parent
4b1c0293a0
commit
3960853e61
5 changed files with 488 additions and 2 deletions
|
|
@ -7,6 +7,7 @@ import { StripeProvider } from './context/StripeContext';
|
|||
import useBrandingSettings from '@hooks/brandingHooks';
|
||||
import imageUtils from '@utils/imageUtils';
|
||||
import Clarity from '@microsoft/clarity';
|
||||
import CookieConsentPopup from '@components/CookieConsentPopup';
|
||||
|
||||
// Import layouts
|
||||
import MainLayout from './layouts/MainLayout';
|
||||
|
|
@ -50,7 +51,6 @@ const BrandingPage = lazy(() => import('@pages/Admin/BrandingPage'));
|
|||
|
||||
const projectId = "rcjhrd0t72"
|
||||
|
||||
Clarity.init(projectId);
|
||||
|
||||
|
||||
// Loading component for suspense fallback
|
||||
|
|
@ -64,6 +64,11 @@ function App() {
|
|||
// Use the centralized hook to fetch branding settings
|
||||
const { data: brandingSettings, isLoading } = useBrandingSettings();
|
||||
|
||||
useEffect(() => {
|
||||
Clarity.init(projectId);
|
||||
Clarity.consent({ clarity: false });
|
||||
}, []);
|
||||
|
||||
// Update the document head with branding settings
|
||||
useEffect(() => {
|
||||
if (brandingSettings) {
|
||||
|
|
@ -114,6 +119,7 @@ function App() {
|
|||
<StripeProvider>
|
||||
<Suspense fallback={<LoadingComponent />}>
|
||||
<Notifications />
|
||||
<CookieConsentPopup />
|
||||
<Routes>
|
||||
{/* Main routes with MainLayout */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
|
|
|
|||
193
frontend/src/components/CookieConsentPopup.jsx
Normal file
193
frontend/src/components/CookieConsentPopup.jsx
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Box,
|
||||
IconButton,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Slide,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import CookieIcon from '@mui/icons-material/Cookie';
|
||||
import consentService, { CONSENT_LEVELS } from '../services/consentService';
|
||||
|
||||
/**
|
||||
* Cookie consent popup component
|
||||
* Shows a popup for cookie consent on first visit and manages Clarity tracking
|
||||
*/
|
||||
const CookieConsentPopup = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [consentLevel, setConsentLevel] = useState(CONSENT_LEVELS.NECESSARY);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has already set cookie preferences
|
||||
const storedConsent = consentService.getStoredConsent();
|
||||
|
||||
if (!storedConsent) {
|
||||
// First visit, show the popup
|
||||
setOpen(true);
|
||||
} else {
|
||||
// Apply stored consent settings
|
||||
setConsentLevel(storedConsent.level);
|
||||
consentService.applyConsentSettings(storedConsent.level);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle consent level change
|
||||
const handleConsentLevelChange = (event) => {
|
||||
setConsentLevel(event.target.value);
|
||||
};
|
||||
|
||||
// Save user preferences and close popup
|
||||
const handleSave = () => {
|
||||
consentService.saveConsent(consentLevel);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// Accept all cookies
|
||||
const handleAcceptAll = () => {
|
||||
const allConsent = CONSENT_LEVELS.ALL;
|
||||
setConsentLevel(allConsent);
|
||||
consentService.saveConsent(allConsent);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// Accept only necessary cookies
|
||||
const handleNecessaryOnly = () => {
|
||||
const necessaryOnly = CONSENT_LEVELS.NECESSARY;
|
||||
setConsentLevel(necessaryOnly);
|
||||
consentService.saveConsent(necessaryOnly);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// Reopen the consent popup (for testing or if user wants to change settings)
|
||||
// This function can be exposed via a preferences button somewhere in your app
|
||||
const reopenConsentPopup = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Slide direction="up" in={open} mountOnEnter unmountOnExit>
|
||||
<Card
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
maxWidth: 400,
|
||||
boxShadow: 4,
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CookieIcon color="primary" fontSize="small" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6">Cookie Settings</Typography>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={() => setOpen(false)}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
We use cookies to enhance your browsing experience, provide personalized content,
|
||||
and analyze our traffic. Please choose your privacy settings below.
|
||||
</Typography>
|
||||
|
||||
<FormControl component="fieldset" sx={{ my: 2 }}>
|
||||
<FormLabel component="legend">Privacy Settings</FormLabel>
|
||||
<RadioGroup
|
||||
value={consentLevel}
|
||||
onChange={handleConsentLevelChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.NECESSARY}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
<strong>Necessary Cookies Only</strong> - Essential for website functionality
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.FUNCTIONAL}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
<strong>Functional Cookies</strong> - For enhanced functionality and preferences
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.ANALYTICS}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
<strong>Analytics Cookies</strong> - To understand how you use our website
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.ALL}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
<strong>All Cookies</strong> - Accept all cookies for the best experience
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleNecessaryOnly}
|
||||
size="small"
|
||||
>
|
||||
Necessary Only
|
||||
</Button>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleSave}
|
||||
sx={{ mr: 1 }}
|
||||
size="small"
|
||||
>
|
||||
Save Preferences
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleAcceptAll}
|
||||
size="small"
|
||||
>
|
||||
Accept All
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
// Expose reopen function for others to use
|
||||
export const reopenCookieConsent = () => {
|
||||
const consentElement = document.getElementById('cookie-consent-reopen');
|
||||
if (consentElement) {
|
||||
consentElement.click();
|
||||
}
|
||||
};
|
||||
|
||||
export default CookieConsentPopup;
|
||||
166
frontend/src/components/CookieSettingsButton.jsx
Normal file
166
frontend/src/components/CookieSettingsButton.jsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, IconButton, Tooltip, Dialog, DialogTitle, DialogContent,
|
||||
DialogActions, Typography, Box, Divider, RadioGroup, Radio, FormControlLabel,
|
||||
FormControl, FormLabel } from '@mui/material';
|
||||
import CookieIcon from '@mui/icons-material/Cookie';
|
||||
import consentService, { CONSENT_LEVELS } from '../services/consentService';
|
||||
|
||||
/**
|
||||
* Button to open cookie settings dialog
|
||||
* Can be placed in the footer or other accessible areas of the app
|
||||
*/
|
||||
const CookieSettingsButton = ({ variant = 'text', icon = false, label = 'Cookie Settings' }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [consentLevel, setConsentLevel] = useState(() => {
|
||||
const storedConsent = consentService.getStoredConsent();
|
||||
return storedConsent?.level || CONSENT_LEVELS.NECESSARY;
|
||||
});
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleConsentLevelChange = (event) => {
|
||||
setConsentLevel(event.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
consentService.saveConsent(consentLevel);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
const allConsent = CONSENT_LEVELS.ALL;
|
||||
setConsentLevel(allConsent);
|
||||
consentService.saveConsent(allConsent);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleNecessaryOnly = () => {
|
||||
const necessaryOnly = CONSENT_LEVELS.NECESSARY;
|
||||
setConsentLevel(necessaryOnly);
|
||||
consentService.saveConsent(necessaryOnly);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
// Render as icon button or regular button
|
||||
if (icon) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={label}>
|
||||
<IconButton onClick={handleOpen} size="small" color="inherit">
|
||||
<CookieIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{renderDialog()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={variant} size="small" onClick={handleOpen}>
|
||||
{label}
|
||||
</Button>
|
||||
{renderDialog()}
|
||||
</>
|
||||
);
|
||||
|
||||
function renderDialog() {
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CookieIcon sx={{ mr: 1 }} />
|
||||
Cookie Settings
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" paragraph>
|
||||
We use cookies to enhance your browsing experience, provide personalized content,
|
||||
and analyze our traffic. You can adjust your privacy settings below.
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<FormControl component="fieldset" sx={{ width: '100%' }}>
|
||||
<FormLabel component="legend">Privacy Settings</FormLabel>
|
||||
<RadioGroup
|
||||
value={consentLevel}
|
||||
onChange={handleConsentLevelChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.NECESSARY}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">Necessary Cookies Only</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Essential cookies required for basic website functionality. These cannot be disabled.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.FUNCTIONAL}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">Functional Cookies</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Cookies that remember your preferences and enhance website functionality.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.ANALYTICS}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">Analytics Cookies</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Cookies that help us understand how you use our website and improve your experience.
|
||||
This includes Microsoft Clarity for analyzing site usage patterns.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={CONSENT_LEVELS.ALL}
|
||||
control={<Radio />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">All Cookies</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Accept all cookies including analytics, marketing, and third-party cookies.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
You can change these settings at any time by clicking on the Cookie Settings link in the footer.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleNecessaryOnly} color="inherit">Necessary Only</Button>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button onClick={handleSave} variant="outlined">Save Preferences</Button>
|
||||
<Button onClick={handleAcceptAll} variant="contained">Accept All</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default CookieSettingsButton;
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Box, Container, Grid, Typography, Link, IconButton } from '@mui/material';
|
||||
import { Box, Container, Grid, Typography, Link, IconButton, Divider } from '@mui/material';
|
||||
import FacebookIcon from '@mui/icons-material/Facebook';
|
||||
import TwitterIcon from '@mui/icons-material/Twitter';
|
||||
import InstagramIcon from '@mui/icons-material/Instagram';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import imageUtils from '@utils/imageUtils';
|
||||
import CookieSettingsButton from './CookieSettingsButton';
|
||||
|
||||
const Footer = ({brandingSettings}) => {
|
||||
|
||||
|
|
@ -89,6 +90,19 @@ const Footer = ({brandingSettings}) => {
|
|||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
{/* <Link component={RouterLink} to="/privacy-policy" color="inherit" variant="body2">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link component={RouterLink} to="/terms-of-service" color="inherit" variant="body2">
|
||||
Terms of Service
|
||||
</Link> */}
|
||||
<CookieSettingsButton />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box mt={3}>
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
{copyrightText}
|
||||
|
|
|
|||
107
frontend/src/services/consentService.js
Normal file
107
frontend/src/services/consentService.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Service for managing cookie and tracking consent preferences
|
||||
*/
|
||||
|
||||
import Clarity from '@microsoft/clarity';
|
||||
|
||||
// Cookie consent levels
|
||||
export const CONSENT_LEVELS = {
|
||||
NECESSARY: 'necessary',
|
||||
FUNCTIONAL: 'functional',
|
||||
ANALYTICS: 'analytics',
|
||||
ALL: 'all'
|
||||
};
|
||||
|
||||
// Local storage key for tracking consent
|
||||
export const CONSENT_STORAGE_KEY = 'rocks_bones_sticks_cookie_consent';
|
||||
|
||||
/**
|
||||
* Get stored consent level
|
||||
* @returns {Object|null} Stored consent data or null if not set
|
||||
*/
|
||||
export const getStoredConsent = () => {
|
||||
const storedConsent = localStorage.getItem(CONSENT_STORAGE_KEY);
|
||||
return storedConsent ? JSON.parse(storedConsent) : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user has given analytics consent
|
||||
* @returns {boolean} Whether analytics is allowed
|
||||
*/
|
||||
export const hasAnalyticsConsent = () => {
|
||||
const consent = getStoredConsent();
|
||||
if (!consent) return false;
|
||||
|
||||
return consent.level === CONSENT_LEVELS.ANALYTICS ||
|
||||
consent.level === CONSENT_LEVELS.ALL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if user has given functional cookies consent
|
||||
* @returns {boolean} Whether functional cookies are allowed
|
||||
*/
|
||||
export const hasFunctionalConsent = () => {
|
||||
const consent = getStoredConsent();
|
||||
if (!consent) return false;
|
||||
|
||||
return consent.level === CONSENT_LEVELS.FUNCTIONAL ||
|
||||
consent.level === CONSENT_LEVELS.ANALYTICS ||
|
||||
consent.level === CONSENT_LEVELS.ALL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply consent settings to tracking tools
|
||||
* @param {string} level - Consent level to apply
|
||||
*/
|
||||
export const applyConsentSettings = (level) => {
|
||||
// Determine if analytics is allowed based on consent level
|
||||
const allowAnalytics = level === CONSENT_LEVELS.ANALYTICS || level === CONSENT_LEVELS.ALL;
|
||||
|
||||
if (allowAnalytics) {
|
||||
// Enable Clarity tracking
|
||||
Clarity.consent();
|
||||
} else {
|
||||
// Disable Clarity tracking
|
||||
Clarity.consent({ clarity: false });
|
||||
}
|
||||
|
||||
console.log(`Applied consent level: ${level}, analytics ${allowAnalytics ? 'enabled' : 'disabled'}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save user consent preferences
|
||||
* @param {string} level - Consent level to save
|
||||
*/
|
||||
export const saveConsent = (level) => {
|
||||
// Save consent to local storage
|
||||
localStorage.setItem(
|
||||
CONSENT_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
level,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
);
|
||||
|
||||
// Apply the consent settings
|
||||
applyConsentSettings(level);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear stored consent preferences
|
||||
*/
|
||||
export const clearConsent = () => {
|
||||
localStorage.removeItem(CONSENT_STORAGE_KEY);
|
||||
|
||||
// Reset to most restrictive level
|
||||
applyConsentSettings(CONSENT_LEVELS.NECESSARY);
|
||||
};
|
||||
|
||||
export default {
|
||||
CONSENT_LEVELS,
|
||||
getStoredConsent,
|
||||
hasAnalyticsConsent,
|
||||
hasFunctionalConsent,
|
||||
applyConsentSettings,
|
||||
saveConsent,
|
||||
clearConsent
|
||||
};
|
||||
Loading…
Reference in a new issue