Files
maternal-app/maternal-web/components/common/banners/CookieConsent.tsx
Andrei e4b97df0c0
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Implement AI response feedback UI and complete high-priority features
Frontend Features:
- Add MessageFeedback component with thumbs up/down buttons
- Positive feedback submits immediately with success toast
- Negative feedback opens dialog for optional text input
- Integrate feedback buttons on all AI assistant messages
- Add success Snackbar confirmation message
- Translation keys added to ai.json (feedback section)

Backend Features:
- Add POST /api/v1/ai/feedback endpoint
- Create FeedbackDto with conversation ID validation
- Implement submitFeedback service method
- Store feedback in conversation metadata with timestamps
- Add audit logging for feedback submissions
- Fix conversationId regex validation to support nanoid format

Legal & Compliance:
- Implement complete EULA acceptance flow with modal
- Create reusable legal content components (Terms, Privacy, EULA)
- Add LegalDocumentViewer for nested modal viewing
- Cookie Consent Banner with GDPR compliance
- Legal pages with AppShell navigation
- EULA acceptance tracking in user entity

Branding Updates:
- Rebrand from "Maternal App" to "ParentFlow"
- Update all icons (72px to 512px) from high-res source
- PWA manifest updated with ParentFlow branding
- Contact email: hello@parentflow.com
- Address: Serbota 3, Bucharest, Romania

Bug Fixes:
- Fix chat endpoint validation (support nanoid conversation IDs)
- Fix EULA acceptance API call (use apiClient vs hardcoded localhost)
- Fix icon loading errors with proper PNG generation

Documentation:
- Mark 11 high-priority features as complete in REMAINING_FEATURES.md
- Update feature statistics: 73/139 complete (53%)
- All high-priority features now complete! 🎉

Files Changed:
Frontend: 21 files (components, pages, locales, icons)
Backend: 6 files (controller, service, DTOs, migrations)
Docs: 1 file (REMAINING_FEATURES.md)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 11:39:02 +00:00

284 lines
8.4 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
Button,
IconButton,
Collapse,
FormControlLabel,
Switch,
Link,
Slide,
} from '@mui/material';
import { Close, Settings as SettingsIcon, Cookie } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
interface CookiePreferences {
essential: boolean; // Always true, cannot be disabled
analytics: boolean;
marketing: boolean;
}
const COOKIE_CONSENT_KEY = 'parentflow_cookie_consent';
const COOKIE_PREFERENCES_KEY = 'parentflow_cookie_preferences';
export function CookieConsent() {
const router = useRouter();
const [showBanner, setShowBanner] = useState(false);
const [showCustomize, setShowCustomize] = useState(false);
const [preferences, setPreferences] = useState<CookiePreferences>({
essential: true,
analytics: false,
marketing: false,
});
useEffect(() => {
// Check if user has already given consent
const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
if (!consent) {
// Show banner after a short delay
const timer = setTimeout(() => setShowBanner(true), 1000);
return () => clearTimeout(timer);
} else {
// Load saved preferences
const savedPrefs = localStorage.getItem(COOKIE_PREFERENCES_KEY);
if (savedPrefs) {
setPreferences(JSON.parse(savedPrefs));
}
}
}, []);
const savePreferences = (prefs: CookiePreferences) => {
localStorage.setItem(COOKIE_CONSENT_KEY, 'true');
localStorage.setItem(COOKIE_PREFERENCES_KEY, JSON.stringify(prefs));
// Apply analytics based on user choice
if (prefs.analytics) {
console.log('📊 Analytics enabled');
// TODO: Initialize analytics (Google Analytics, etc.)
} else {
console.log('📊 Analytics disabled');
// TODO: Disable analytics
}
setShowBanner(false);
};
const handleAcceptAll = () => {
const allPrefs: CookiePreferences = {
essential: true,
analytics: true,
marketing: true,
};
setPreferences(allPrefs);
savePreferences(allPrefs);
};
const handleRejectAll = () => {
const essentialOnly: CookiePreferences = {
essential: true,
analytics: false,
marketing: false,
};
setPreferences(essentialOnly);
savePreferences(essentialOnly);
};
const handleSaveCustom = () => {
savePreferences(preferences);
};
const handleToggleCustomize = () => {
setShowCustomize(!showCustomize);
};
if (!showBanner) {
return null;
}
return (
<Slide direction="up" in={showBanner} mountOnEnter unmountOnExit>
<Paper
elevation={8}
sx={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: (theme) => theme.zIndex.snackbar,
borderRadius: '16px 16px 0 0',
maxWidth: { xs: '100%', md: 600 },
mx: 'auto',
mb: 0,
}}
>
<Box sx={{ p: 3, position: 'relative' }}>
{/* Close button */}
<IconButton
onClick={handleRejectAll}
sx={{ position: 'absolute', top: 8, right: 8 }}
size="small"
aria-label="Reject all cookies"
>
<Close />
</IconButton>
{/* Cookie icon and title */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Cookie sx={{ color: 'primary.main', fontSize: 28 }} />
<Typography variant="h6" fontWeight="600">
Cookie Preferences
</Typography>
</Box>
{/* Description */}
<Typography variant="body2" color="text.secondary" paragraph>
We use cookies to enhance your experience, analyze site usage, and assist in our marketing efforts.
Essential cookies are required for the app to function.{' '}
<Link
href="/legal/cookies"
onClick={(e) => {
e.preventDefault();
router.push('/legal/cookies');
}}
sx={{ fontWeight: 'bold', textDecoration: 'underline', cursor: 'pointer' }}
>
Learn more
</Link>
</Typography>
{/* Customize section */}
<Collapse in={showCustomize}>
<Box sx={{ my: 2, p: 2, bgcolor: 'background.default', borderRadius: 2 }}>
<FormControlLabel
control={
<Switch
checked={preferences.essential}
disabled
color="primary"
/>
}
label={
<Box>
<Typography variant="body2" fontWeight="600">
Essential Cookies
</Typography>
<Typography variant="caption" color="text.secondary">
Required for the app to function. Cannot be disabled.
</Typography>
</Box>
}
sx={{ mb: 1, alignItems: 'flex-start' }}
/>
<FormControlLabel
control={
<Switch
checked={preferences.analytics}
onChange={(e) => setPreferences({ ...preferences, analytics: e.target.checked })}
color="primary"
/>
}
label={
<Box>
<Typography variant="body2" fontWeight="600">
Analytics Cookies
</Typography>
<Typography variant="caption" color="text.secondary">
Help us understand how you use the app (anonymized data).
</Typography>
</Box>
}
sx={{ mb: 1, alignItems: 'flex-start' }}
/>
<FormControlLabel
control={
<Switch
checked={preferences.marketing}
onChange={(e) => setPreferences({ ...preferences, marketing: e.target.checked })}
color="primary"
/>
}
label={
<Box>
<Typography variant="body2" fontWeight="600">
Marketing Cookies
</Typography>
<Typography variant="caption" color="text.secondary">
Used to deliver relevant content and track campaign performance.
</Typography>
</Box>
}
sx={{ alignItems: 'flex-start' }}
/>
</Box>
</Collapse>
{/* Action buttons */}
<Box
sx={{
display: 'flex',
gap: 1,
flexDirection: { xs: 'column', sm: 'row' },
mt: 2,
}}
>
{showCustomize ? (
<>
<Button
variant="outlined"
onClick={handleToggleCustomize}
fullWidth
sx={{ textTransform: 'none' }}
>
Cancel
</Button>
<Button
variant="contained"
onClick={handleSaveCustom}
fullWidth
sx={{ textTransform: 'none' }}
>
Save Preferences
</Button>
</>
) : (
<>
<Button
variant="outlined"
onClick={handleRejectAll}
fullWidth
sx={{ textTransform: 'none' }}
>
Reject All
</Button>
<Button
variant="outlined"
startIcon={<SettingsIcon />}
onClick={handleToggleCustomize}
fullWidth
sx={{ textTransform: 'none' }}
>
Customize
</Button>
<Button
variant="contained"
onClick={handleAcceptAll}
fullWidth
sx={{ textTransform: 'none' }}
>
Accept All
</Button>
</>
)}
</Box>
</Box>
</Paper>
</Slide>
);
}