feat: Redesign mobile UI with centered voice button and user menu
- 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:
@@ -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're excited to help you track and understand your child'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'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'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'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're All Set! 🎉
|
||||
{t('complete.title')} 🎉
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||
Start tracking your child'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>
|
||||
|
||||
Reference in New Issue
Block a user