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

@@ -38,19 +38,70 @@ const steps = ['Welcome', 'Language', 'Measurements', 'Add Child', 'Complete'];
export default function OnboardingPage() {
const [activeStep, setActiveStep] = useState(0);
const [selectedLanguage, setSelectedLanguage] = useState('en');
const [selectedMeasurement, setSelectedMeasurement] = useState<MeasurementSystem>('metric');
const [childName, setChildName] = useState('');
const [childBirthDate, setChildBirthDate] = useState('');
const [childGender, setChildGender] = useState<'male' | 'female' | 'other'>('other');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const { user } = useAuth();
const { user, refreshUser } = useAuth();
const { setLanguage, setMeasurementSystem } = useLocale();
const { t } = useTranslation('onboarding');
const handleNext = async () => {
// Validate and save child data on step 1 (Add Child)
setError('');
// Step 1: Save language preference
if (activeStep === 1) {
try {
setLoading(true);
await setLanguage(selectedLanguage);
// Save to backend
if (user?.id) {
await usersApi.updatePreferences({
language: selectedLanguage,
});
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
} catch (err: any) {
console.error('Failed to save language:', err);
setError('Failed to save language preference. Continuing anyway...');
setTimeout(() => setActiveStep((prevActiveStep) => prevActiveStep + 1), 1500);
} finally {
setLoading(false);
}
return;
}
// Step 2: Save measurement preference
if (activeStep === 2) {
try {
setLoading(true);
setMeasurementSystem(selectedMeasurement);
// Save to backend
if (user?.id) {
await usersApi.updatePreferences({
measurementUnit: selectedMeasurement,
});
await refreshUser();
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
} catch (err: any) {
console.error('Failed to save measurement:', err);
setError('Failed to save measurement preference. Continuing anyway...');
setTimeout(() => setActiveStep((prevActiveStep) => prevActiveStep + 1), 1500);
} finally {
setLoading(false);
}
return;
}
// Step 3: Validate and save child data (Add Child)
if (activeStep === 3) {
if (!childName.trim() || !childBirthDate) {
setError('Please enter child name and birth date');
setError(t('child.name') + ' and ' + t('child.dateOfBirth') + ' are required');
return;
}
@@ -118,11 +169,20 @@ export default function OnboardingPage() {
}}
>
<Stepper activeStep={activeStep} sx={{ mb: 4 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
{steps.map((label, index) => {
let stepLabel = label;
if (index === 0) stepLabel = t('welcome.title').split('!')[0];
else if (index === 1) stepLabel = t('language.title');
else if (index === 2) stepLabel = t('measurements.title');
else if (index === 3) stepLabel = t('child.title');
else if (index === 4) stepLabel = t('complete.title').split('!')[0];
return (
<Step key={label}>
<StepLabel>{stepLabel}</StepLabel>
</Step>
);
})}
</Stepper>
<AnimatePresence mode="wait">
@@ -133,18 +193,19 @@ export default function OnboardingPage() {
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
{/* Step 0: Welcome */}
{activeStep === 0 && (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h4" gutterBottom fontWeight="600" color="primary.main">
Welcome to Maternal! 🎉
{t('welcome.title')} 🎉
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 2, mb: 4 }}>
We&apos;re excited to help you track and understand your child&apos;s development, sleep patterns, feeding schedules, and more.
{t('welcome.description')}
</Typography>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
<Paper sx={{ p: 2, flex: 1, minWidth: 150 }}>
<Typography variant="h6" fontWeight="600">📊</Typography>
<Typography variant="body2">Track Activities</Typography>
<Typography variant="body2">{t('welcome.getStarted')}</Typography>
</Paper>
<Paper sx={{ p: 2, flex: 1, minWidth: 150 }}>
<Typography variant="h6" fontWeight="600">🤖</Typography>
@@ -158,13 +219,165 @@ export default function OnboardingPage() {
</Box>
)}
{/* Step 1: Language Selection */}
{activeStep === 1 && (
<Box sx={{ py: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Language sx={{ fontSize: 40, color: 'primary.main', mr: 2 }} />
<Box>
<Typography variant="h5" fontWeight="600">
{t('language.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('language.subtitle')}
</Typography>
</Box>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
{error}
</Alert>
)}
<Grid container spacing={2} sx={{ mt: 2 }}>
{supportedLanguages.map((lang) => (
<Grid item xs={12} sm={6} key={lang.code}>
<Card
sx={{
border: selectedLanguage === lang.code ? '2px solid' : '1px solid',
borderColor: selectedLanguage === lang.code ? 'primary.main' : 'divider',
transition: 'all 0.2s',
}}
>
<CardActionArea
onClick={() => setSelectedLanguage(lang.code)}
sx={{ p: 2 }}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="h6" fontWeight="600">
{lang.nativeName}
</Typography>
<Typography variant="body2" color="text.secondary">
{lang.name}
</Typography>
</Box>
<Radio
checked={selectedLanguage === lang.code}
value={lang.code}
name="language-radio"
/>
</Box>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
{t('language.description')}
</Alert>
</Box>
)}
{/* Step 2: Measurement System */}
{activeStep === 2 && (
<Box sx={{ py: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Straighten sx={{ fontSize: 40, color: 'primary.main', mr: 2 }} />
<Box>
<Typography variant="h5" fontWeight="600">
{t('measurements.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('measurements.subtitle')}
</Typography>
</Box>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2, borderRadius: 2 }}>
{error}
</Alert>
)}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12} sm={6}>
<Card
sx={{
border: selectedMeasurement === 'metric' ? '2px solid' : '1px solid',
borderColor: selectedMeasurement === 'metric' ? 'primary.main' : 'divider',
height: '100%',
}}
>
<CardActionArea
onClick={() => setSelectedMeasurement('metric')}
sx={{ p: 3, height: '100%' }}
>
<Box sx={{ textAlign: 'center' }}>
<Radio
checked={selectedMeasurement === 'metric'}
value="metric"
name="measurement-radio"
sx={{ mb: 1 }}
/>
<Typography variant="h6" fontWeight="600" gutterBottom>
{t('measurements.metric.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('measurements.metric.description')}
</Typography>
</Box>
</CardActionArea>
</Card>
</Grid>
<Grid item xs={12} sm={6}>
<Card
sx={{
border: selectedMeasurement === 'imperial' ? '2px solid' : '1px solid',
borderColor: selectedMeasurement === 'imperial' ? 'primary.main' : 'divider',
height: '100%',
}}
>
<CardActionArea
onClick={() => setSelectedMeasurement('imperial')}
sx={{ p: 3, height: '100%' }}
>
<Box sx={{ textAlign: 'center' }}>
<Radio
checked={selectedMeasurement === 'imperial'}
value="imperial"
name="measurement-radio"
sx={{ mb: 1 }}
/>
<Typography variant="h6" fontWeight="600" gutterBottom>
{t('measurements.imperial.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('measurements.imperial.description')}
</Typography>
</Box>
</CardActionArea>
</Card>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
{t('measurements.description')}
</Alert>
</Box>
)}
{/* Step 3: Add Child */}
{activeStep === 3 && (
<Box sx={{ py: 4 }}>
<Typography variant="h5" gutterBottom fontWeight="600">
Add Your First Child
{t('child.title')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Let&apos;s start by adding some basic information about your child.
{t('child.subtitle')}
</Typography>
{error && (
@@ -175,7 +388,7 @@ export default function OnboardingPage() {
<TextField
fullWidth
label="Child&apos;s Name"
label={t('child.name')}
value={childName}
onChange={(e) => setChildName(e.target.value)}
margin="normal"
@@ -188,7 +401,7 @@ export default function OnboardingPage() {
<TextField
fullWidth
label="Birth Date"
label={t('child.dateOfBirth')}
type="date"
value={childBirthDate}
onChange={(e) => setChildBirthDate(e.target.value)}
@@ -206,7 +419,7 @@ export default function OnboardingPage() {
<TextField
fullWidth
select
label="Gender"
label={t('child.gender')}
value={childGender}
onChange={(e) => setChildGender(e.target.value as 'male' | 'female' | 'other')}
margin="normal"
@@ -215,52 +428,19 @@ export default function OnboardingPage() {
sx: { borderRadius: 3 },
}}
>
<MenuItem value="male">Male</MenuItem>
<MenuItem value="female">Female</MenuItem>
<MenuItem value="other">Prefer not to say</MenuItem>
<MenuItem value="male">{t('child.genders.male')}</MenuItem>
<MenuItem value="female">{t('child.genders.female')}</MenuItem>
<MenuItem value="other">{t('child.genders.preferNotToSay')}</MenuItem>
</TextField>
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
You can add more children and details later from settings.
{t('child.skipForNow')}
</Alert>
</Box>
)}
{activeStep === 2 && (
<Box sx={{ py: 4 }}>
<Typography variant="h5" gutterBottom fontWeight="600">
Invite Family Members
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Share your child&apos;s progress with family members. They can view activities and add their own entries.
</Typography>
<TextField
fullWidth
label="Email Address"
type="email"
margin="normal"
placeholder="partner@example.com"
InputProps={{
sx: { borderRadius: 3 },
}}
/>
<Button
variant="outlined"
fullWidth
sx={{ mt: 2 }}
>
Send Invitation
</Button>
<Alert severity="info" sx={{ mt: 3, borderRadius: 2 }}>
You can skip this step and invite family members later.
</Alert>
</Box>
)}
{activeStep === 3 && (
{/* Step 4: Complete */}
{activeStep === 4 && (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Avatar
sx={{
@@ -274,15 +454,15 @@ export default function OnboardingPage() {
<Check sx={{ fontSize: 48 }} />
</Avatar>
<Typography variant="h5" gutterBottom fontWeight="600">
You&apos;re All Set! 🎉
{t('complete.title')} 🎉
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
Start tracking your child&apos;s activities and get personalized insights.
{t('complete.description')}
</Typography>
<Paper sx={{ p: 3, bgcolor: 'primary.light', mb: 3 }}>
<Typography variant="body2" fontWeight="600" gutterBottom>
Next Steps:
{t('complete.subtitle')}
</Typography>
<Typography variant="body2" align="left" component="div">
Track your first feeding, sleep, or diaper change<br />
@@ -301,14 +481,14 @@ export default function OnboardingPage() {
disabled={activeStep === 0}
startIcon={<ArrowBack />}
>
Back
{t('navigation.back')}
</Button>
<Box sx={{ flex: 1 }} />
{activeStep < steps.length - 1 && activeStep > 0 && (
{activeStep === 3 && (
<Button onClick={handleSkip} sx={{ mr: 2 }}>
Skip
{t('navigation.skip')}
</Button>
)}
@@ -318,7 +498,7 @@ export default function OnboardingPage() {
disabled={loading}
endIcon={loading ? <CircularProgress size={20} /> : (activeStep === steps.length - 1 ? <Check /> : <ArrowForward />)}
>
{activeStep === steps.length - 1 ? 'Get Started' : 'Next'}
{activeStep === steps.length - 1 ? t('complete.startTracking') : t('navigation.next')}
</Button>
</Box>
</Paper>