UI improvements for photo upload feature: 1. Hidden the photo URL text field completely (users don't need to see base64) 2. Added key prop to Avatar to force re-render when photoUrl changes 3. Added error handling for avatar image loading 4. Changed value display to hide base64 strings in hidden field The camera icon is now the primary way to upload photos, with the URL field completely hidden from view for a cleaner UX. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
6.2 KiB
TypeScript
211 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState } 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 } 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);
|
|
|
|
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={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>
|
|
);
|
|
};
|