feat: Redesign mobile UI with centered voice button and user menu
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

- Repositioned Voice Command button to center of bottom navigation bar
- Added floating user menu icon in top-left corner on mobile
- User menu includes: Settings, Children, Family, and Logout options
- Updated bottom nav to show: Home, Track, Voice (center), Insights, History
- Hide original floating voice button on mobile to avoid duplication
- Improved mobile UX with easier thumb access to voice commands
- User avatar displays first letter of user's name

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 15:06:46 +00:00
parent 58c3a8d9d5
commit 8f150cbf59
5 changed files with 585 additions and 218 deletions

View File

@@ -1,13 +1,28 @@
'use client';
import { Box, Container, Chip, Tooltip } from '@mui/material';
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 } from '@mui/icons-material';
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;
@@ -15,9 +30,31 @@ interface AppShellProps {
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={{
@@ -29,6 +66,85 @@ export const AppShell = ({ children }: AppShellProps) => {
}}>
{!isMobile && <MobileNav />}
{/* Mobile User Menu Button - Top Left */}
{isMobile && (
<Box
sx={{
position: 'fixed',
top: 8,
left: 8,
zIndex: 1200,
}}
>
<IconButton
onClick={handleMenuOpen}
size="small"
aria-label="user menu"
aria-controls={anchorEl ? 'user-menu' : undefined}
aria-haspopup="true"
aria-expanded={anchorEl ? 'true' : undefined}
sx={{
bgcolor: 'background.paper',
boxShadow: 1,
'&:hover': {
bgcolor: 'background.paper',
boxShadow: 2,
},
}}
>
<Avatar
sx={{
width: 32,
height: 32,
bgcolor: 'primary.main',
fontSize: '0.875rem',
}}
>
{user?.name?.charAt(0).toUpperCase() || 'U'}
</Avatar>
</IconButton>
<Menu
id="user-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
onClick={handleMenuClose}
transformOrigin={{ horizontal: 'left', vertical: 'top' }}
anchorOrigin={{ horizontal: 'left', 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>
)}
{/* Connection Status & Presence Indicator */}
<Box
sx={{

View File

@@ -1,13 +1,14 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { BottomNavigation, BottomNavigationAction, Paper } from '@mui/material';
import { useState } from 'react';
import { BottomNavigation, BottomNavigationAction, Paper, Fab, Box } from '@mui/material';
import {
Home,
Timeline,
Chat,
Insights,
Settings,
History,
Mic,
} from '@mui/icons-material';
import { useTranslation } from '@/hooks/useTranslation';
@@ -15,53 +16,102 @@ export const TabBar = () => {
const { t } = useTranslation('common');
const router = useRouter();
const pathname = usePathname();
const [voiceOpen, setVoiceOpen] = useState(false);
const tabs = [
{ label: t('navigation.home'), icon: <Home />, value: '/' },
{ label: t('navigation.track'), icon: <Timeline />, value: '/track' },
{ label: t('navigation.aiChat'), icon: <Chat />, value: '/ai-assistant' },
{ label: '', icon: null, value: 'voice' }, // Placeholder for center button
{ label: t('navigation.insights'), icon: <Insights />, value: '/insights' },
{ label: t('navigation.settings'), icon: <Settings />, value: '/settings' },
{ label: t('navigation.history'), icon: <History />, value: '/history' },
];
return (
<Paper
component="nav"
aria-label="Primary navigation"
sx={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
}}
elevation={3}
>
<BottomNavigation
value={pathname}
onChange={(event, newValue) => {
router.push(newValue);
}}
showLabels
<>
<Paper
component="nav"
aria-label="Primary navigation"
sx={{
height: 64,
'& .MuiBottomNavigationAction-root': {
minWidth: 60,
'&.Mui-selected': {
color: 'primary.main',
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
zIndex: 1000,
}}
elevation={3}
>
<BottomNavigation
value={pathname}
onChange={(event, newValue) => {
if (newValue !== 'voice') {
router.push(newValue);
}
}}
showLabels
sx={{
height: 64,
'& .MuiBottomNavigationAction-root': {
minWidth: 60,
'&.Mui-selected': {
color: 'primary.main',
},
},
}}
>
{tabs.map((tab) => {
if (tab.value === 'voice') {
// Center voice button placeholder
return (
<Box
key="voice-placeholder"
sx={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
/>
);
}
return (
<BottomNavigationAction
key={tab.value}
label={tab.label}
icon={tab.icon}
value={tab.value}
/>
);
})}
</BottomNavigation>
</Paper>
{/* Voice Command Floating Button - Centered */}
<Fab
color="secondary"
aria-label="voice command"
onClick={() => {
// Trigger voice command - will integrate with existing VoiceFloatingButton
const voiceButton = document.querySelector('[aria-label="voice input"]') as HTMLButtonElement;
if (voiceButton) {
voiceButton.click();
}
}}
sx={{
position: 'fixed',
bottom: 40,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1100,
width: 56,
height: 56,
bgcolor: 'secondary.main',
'&:hover': {
bgcolor: 'secondary.dark',
},
}}
>
{tabs.map((tab) => (
<BottomNavigationAction
key={tab.value}
label={tab.label}
icon={tab.icon}
value={tab.value}
/>
))}
</BottomNavigation>
</Paper>
<Mic />
</Fab>
</>
);
};

View File

@@ -323,7 +323,7 @@ export function VoiceFloatingButton() {
return (
<>
{/* Floating button positioned in bottom-right */}
{/* Floating button positioned in bottom-right - Hidden on mobile since we have TabBar center button */}
<Tooltip title="Voice Command (Beta)" placement="left">
<Fab
color="primary"
@@ -335,6 +335,7 @@ export function VoiceFloatingButton() {
bottom: 24,
right: 24,
zIndex: 1000,
display: { xs: 'none', md: 'flex' }, // Hide on mobile (xs) and small screens, show on medium+
}}
>
<MicIcon />