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:
mav
2026-05-28 17:11:27 +02:00
parent 6311088256
commit 93042cbb4c
26 changed files with 2582 additions and 2 deletions

View File

@@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../theme.dart';
import '../../widgets/mascot.dart';
import '../../model/quiz_state.dart';
import '../../router.dart';
import 's15_result.dart';
class AnalysingScreen extends StatefulWidget {
final QuizState state;
const AnalysingScreen({super.key, required this.state});
@override
State<AnalysingScreen> createState() => _State();
}
class _State extends State<AnalysingScreen> {
int _tipIndex = 0;
double _progress = 0.0;
static const _tips = [
'Reading your skin profile...',
'Matching 10,000+ ingredients...',
'Building your morning routine...',
'Building your evening routine...',
'Finalising your report... ✨',
];
@override
void initState() {
super.initState();
_cycle();
// Progress animation
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) setState(() => _progress = 1.0);
});
Future.delayed(const Duration(milliseconds: 3200), () {
if (mounted) Navigator.of(context).pushReplacement(fadeRoute(ResultScreen(state: widget.state)));
});
}
void _cycle() async {
for (var i = 0; i < _tips.length; i++) {
await Future.delayed(const Duration(milliseconds: 600));
if (mounted) setState(() => _tipIndex = i);
}
}
Route<dynamic> fadeRoute(Widget page) => PageRouteBuilder(
pageBuilder: (_, a, __) => page,
transitionsBuilder: (_, a, __, child) => FadeTransition(opacity: a, child: child),
transitionDuration: const Duration(milliseconds: 500),
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: SColors.bgSurface,
body: SafeArea(
child: Column(
children: [
const Spacer(flex: 2),
Mascot(mood: MascotMood.analyze, size: 180)
.animate().fadeIn(duration: 600.ms),
const SizedBox(height: 40),
// Progress bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 48),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: AnimatedContainer(
duration: const Duration(milliseconds: 3000),
curve: Curves.easeInOut,
height: 4,
child: LinearProgressIndicator(
value: _progress,
backgroundColor: SColors.borderLight,
valueColor: const AlwaysStoppedAnimation<Color>(SColors.primary),
minHeight: 4,
),
),
),
),
const SizedBox(height: 24),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
_tips[_tipIndex],
key: ValueKey(_tipIndex),
style: SText.title(SColors.textHead),
textAlign: TextAlign.center,
).animate().fadeIn(duration: 250.ms).slideY(begin: 0.1, end: 0),
),
const SizedBox(height: 8),
Text(
'This takes just a few seconds',
style: SText.subtitle(SColors.textSub),
),
const Spacer(flex: 3),
],
),
),
);
}
}