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>
This commit is contained in:
283
maternal-web/components/common/banners/CookieConsent.tsx
Normal file
283
maternal-web/components/common/banners/CookieConsent.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user