feat: complete 20-screen onboarding flow
Full quiz flow s01-s20: splash, intro, value prop, social proof, name/skin-type/age/concerns/goal quiz with insights, analysing loader, result with skin score, AM/PM routines, ingredient science, signup, welcome. Duolingo-style design: flat cards, pressable SButton (Stack trick), 4px progress bar, mascot moods (wave/think/analyze/celebrate), confetti on result+welcome, personalized content throughout.
This commit is contained in:
129
lib/screens/onboarding/s16_routine_am.dart
Normal file
129
lib/screens/onboarding/s16_routine_am.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../theme.dart';
|
||||
import '../../widgets/s_button.dart';
|
||||
import '../../model/quiz_state.dart';
|
||||
import '../../router.dart';
|
||||
import 's17_routine_pm.dart';
|
||||
|
||||
class RoutineAmScreen extends StatelessWidget {
|
||||
final QuizState state;
|
||||
const RoutineAmScreen({super.key, required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: SColors.bgSurface,
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.30,
|
||||
child: _IllustrationZone().animate().fadeIn(duration: 500.ms),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_badge('🌅 Morning routine')
|
||||
.animate().fadeIn(duration: 350.ms, delay: 100.ms),
|
||||
const SizedBox(height: 14),
|
||||
Text('Your AM\nroutine 🌞', style: SText.headline2(SColors.textHead))
|
||||
.animate().fadeIn(duration: 400.ms, delay: 180.ms)
|
||||
.slideY(begin: 0.05, end: 0),
|
||||
const SizedBox(height: 6),
|
||||
Text('4 steps · ~5 minutes · every morning', style: SText.subtitle(SColors.textSub))
|
||||
.animate().fadeIn(duration: 400.ms, delay: 260.ms),
|
||||
const SizedBox(height: 20),
|
||||
..._amSteps(state).asMap().entries.map((e) =>
|
||||
_stepRow(e.value.$1, e.value.$2, e.value.$3, e.key)
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 32),
|
||||
child: SButton(
|
||||
label: 'SEE EVENING ROUTINE',
|
||||
onTap: () => Navigator.of(context).push(slideRoute(RoutinePmScreen(state: state))),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 600.ms),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<(String, String, String)> _amSteps(QuizState state) {
|
||||
final hasDark = state.concerns.contains('Dark spots');
|
||||
final hasBreakouts = state.concerns.contains('Breakouts');
|
||||
return [
|
||||
('1', 'Cleanser', hasBreakouts ? 'Gentle foaming, fragrance-free' : 'Hydrating, pH-balanced'),
|
||||
('2', 'Vitamin C serum', hasDark ? 'High-priority for dark spots' : 'Brightening & antioxidant'),
|
||||
('3', 'Moisturiser', state.skinType == 'Oily' ? 'Lightweight gel formula' : 'Cream formula, hydrating'),
|
||||
('4', 'SPF 50+', 'Non-negotiable. Protects & prevents'),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _stepRow(String num, String name, String sub, int i) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: SColors.bgSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: SColors.borderLight, width: 1.5),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32, height: 32,
|
||||
decoration: const BoxDecoration(color: SColors.primaryLight, shape: BoxShape.circle),
|
||||
child: Center(child: Text(num, style: SText.cardLabel(SColors.primary).copyWith(fontSize: 14))),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(name, style: SText.cardLabel(SColors.textHead)),
|
||||
Text(sub, style: SText.cardSub(SColors.textSub)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: SColors.textSub, size: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate().fadeIn(duration: 280.ms, delay: (300 + i * 80).ms)
|
||||
.slideX(begin: 0.03, end: 0, duration: 280.ms);
|
||||
}
|
||||
|
||||
Widget _badge(String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3CD),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(label, style: SText.caption(const Color(0xFF856404)).copyWith(fontWeight: FontWeight.w700)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IllustrationZone extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: const Color(0xFFFFF9F0),
|
||||
width: double.infinity,
|
||||
child: Center(
|
||||
child: const Text('🌅', style: TextStyle(fontSize: 80))
|
||||
.animate(onPlay: (c) => c.repeat(reverse: true))
|
||||
.moveY(begin: 0, end: -8, duration: 2000.ms, curve: Curves.easeInOut),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user