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>
227 lines
6.8 KiB
TypeScript
227 lines
6.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Container,
|
|
Chip,
|
|
Tooltip,
|
|
IconButton,
|
|
Menu,
|
|
MenuItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Avatar,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import { MobileNav } from '../MobileNav/MobileNav';
|
|
import { TabBar } from '../TabBar/TabBar';
|
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
import { ReactNode } from 'react';
|
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
|
import { Wifi, WifiOff, People, AccountCircle, Settings, ChildCare, Group, Logout, Gavel } from '@mui/icons-material';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
|
|
interface AppShellProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export const AppShell = ({ children }: AppShellProps) => {
|
|
const { t } = useTranslation('common');
|
|
const router = useRouter();
|
|
const { user, logout } = useAuth();
|
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
|
const isTablet = useMediaQuery('(max-width: 1024px)');
|
|
const { isConnected, presence } = useWebSocket();
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
|
|
// Debug: Log user photo changes
|
|
useEffect(() => {
|
|
console.log('👤 User updated in AppShell:', {
|
|
name: user?.name,
|
|
hasPhoto: !!user?.photoUrl,
|
|
photoPreview: user?.photoUrl?.substring(0, 50)
|
|
});
|
|
}, [user?.photoUrl]);
|
|
|
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const handleNavigate = (path: string) => {
|
|
handleMenuClose();
|
|
router.push(path);
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
handleMenuClose();
|
|
await logout();
|
|
router.push('/login');
|
|
};
|
|
|
|
return (
|
|
<Box sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
minHeight: '100vh',
|
|
bgcolor: 'background.default',
|
|
pb: '64px', // Space for tab bar on both mobile and desktop
|
|
}}>
|
|
{/* Header Bar - Both Mobile and Desktop */}
|
|
<Box
|
|
sx={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 48,
|
|
bgcolor: 'background.paper',
|
|
borderBottom: '1px solid',
|
|
borderColor: 'divider',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
px: 2,
|
|
zIndex: 1200,
|
|
boxShadow: 1,
|
|
}}
|
|
>
|
|
{/* Left Side - Family Members Online Indicator */}
|
|
<Box>
|
|
{isConnected && presence.count > 1 && (
|
|
<Tooltip title={t('connection.familyMembersOnline', { count: presence.count })}>
|
|
<Chip
|
|
icon={<People />}
|
|
label={presence.count}
|
|
size="small"
|
|
color="primary"
|
|
sx={{
|
|
fontWeight: 600,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Right Side - User Menu Button with Status Indicator */}
|
|
<Tooltip title={isConnected ? t('connection.syncActive') : t('connection.syncDisconnected')}>
|
|
<IconButton
|
|
onClick={handleMenuOpen}
|
|
size="medium"
|
|
aria-label="user menu"
|
|
aria-controls={anchorEl ? 'user-menu' : undefined}
|
|
aria-haspopup="true"
|
|
aria-expanded={anchorEl ? 'true' : undefined}
|
|
sx={{
|
|
minWidth: 44,
|
|
minHeight: 44,
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<Avatar
|
|
src={user?.photoUrl || undefined}
|
|
key={user?.photoUrl || 'no-photo'} // Force re-render when photoUrl changes
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
bgcolor: 'primary.main',
|
|
fontSize: '0.875rem',
|
|
}}
|
|
imgProps={{
|
|
onError: (e: any) => {
|
|
console.error('Avatar image failed to load:', user?.photoUrl?.substring(0, 50));
|
|
e.target.style.display = 'none'; // Hide broken image, show fallback
|
|
}
|
|
}}
|
|
>
|
|
{user?.name?.charAt(0).toUpperCase() || 'U'}
|
|
</Avatar>
|
|
{/* Status Dot Indicator */}
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: 2,
|
|
right: 2,
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: '50%',
|
|
bgcolor: isConnected ? '#4caf50' : '#9e9e9e',
|
|
border: '2px solid',
|
|
borderColor: 'background.paper',
|
|
boxShadow: 1,
|
|
}}
|
|
aria-label={isConnected ? 'Online' : 'Offline'}
|
|
/>
|
|
</IconButton>
|
|
</Tooltip>
|
|
|
|
<Menu
|
|
id="user-menu"
|
|
anchorEl={anchorEl}
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleMenuClose}
|
|
onClick={handleMenuClose}
|
|
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
|
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
|
sx={{
|
|
mt: 1,
|
|
}}
|
|
>
|
|
<MenuItem onClick={() => handleNavigate('/settings')}>
|
|
<ListItemIcon>
|
|
<Settings fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t('navigation.settings')}</ListItemText>
|
|
</MenuItem>
|
|
<MenuItem onClick={() => handleNavigate('/children')}>
|
|
<ListItemIcon>
|
|
<ChildCare fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t('navigation.children')}</ListItemText>
|
|
</MenuItem>
|
|
<MenuItem onClick={() => handleNavigate('/family')}>
|
|
<ListItemIcon>
|
|
<Group fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t('navigation.family')}</ListItemText>
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={() => handleNavigate('/legal/privacy')}>
|
|
<ListItemIcon>
|
|
<Gavel fontSize="small" />
|
|
</ListItemIcon>
|
|
<ListItemText>Legal & Privacy</ListItemText>
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={handleLogout}>
|
|
<ListItemIcon>
|
|
<Logout fontSize="small" color="error" />
|
|
</ListItemIcon>
|
|
<ListItemText>{t('navigation.logout')}</ListItemText>
|
|
</MenuItem>
|
|
</Menu>
|
|
</Box>
|
|
|
|
<Container
|
|
maxWidth={isTablet ? 'md' : 'lg'}
|
|
sx={{
|
|
flex: 1,
|
|
px: { xs: 2, md: 3 },
|
|
py: 3,
|
|
pt: '64px', // Add top padding for header bar on both mobile and desktop
|
|
}}
|
|
>
|
|
{children}
|
|
</Container>
|
|
|
|
<TabBar />
|
|
</Box>
|
|
);
|
|
};
|