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>
284 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|