Files
mav 93042cbb4c 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.
2026-05-28 17:11:27 +02:00

135 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../theme.dart';
import '../../widgets/mascot.dart';
import '../../widgets/s_button.dart';
import '../../widgets/s_progress_bar.dart';
import '../../model/quiz_state.dart';
import '../../router.dart';
import 's06_skin_type.dart';
class NameScreen extends StatefulWidget {
final QuizState state;
NameScreen({super.key, QuizState? state}) : state = state ?? QuizState();
@override
State<NameScreen> createState() => _State();
}
class _State extends State<NameScreen> {
late final QuizState _quiz;
final _ctrl = TextEditingController();
bool _ready = false;
@override
void initState() {
super.initState();
_quiz = widget.state;
_ctrl.addListener(() => setState(() => _ready = _ctrl.text.trim().length >= 2));
}
@override
void dispose() { _ctrl.dispose(); super.dispose(); }
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: SColors.bgSurface,
resizeToAvoidBottomInset: true,
body: SafeArea(
child: Column(
children: [
_nav(),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SProgressBar(value: 1 / 7),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 24),
Center(
child: Mascot(mood: MascotMood.wave, size: 130)
.animate().fadeIn(duration: 500.ms).slideY(begin: 0.05, end: 0),
),
const SizedBox(height: 24),
Text("Hey! What's\nyour name? 👋", style: SText.headline2(SColors.textHead))
.animate().fadeIn(duration: 400.ms, delay: 150.ms),
const SizedBox(height: 8),
Text("I'll personalise everything just for you.", style: SText.subtitle(SColors.textSub))
.animate().fadeIn(duration: 400.ms, delay: 220.ms),
const SizedBox(height: 28),
_nameInput().animate().fadeIn(duration: 400.ms, delay: 300.ms),
const SizedBox(height: 16),
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 20),
child: SButton(
label: 'CONTINUE',
enabled: _ready,
onTap: _ready ? () {
_quiz.name = _ctrl.text.trim();
Navigator.of(context).push(slideRoute(SkinTypeScreen(state: _quiz)));
} : null,
).animate().fadeIn(duration: 400.ms, delay: 400.ms),
),
],
),
),
);
}
Widget _nav() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 36, height: 36,
decoration: BoxDecoration(
color: SColors.bgSurface,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: SColors.borderLight, width: 1.5),
),
child: const Icon(Icons.arrow_back_ios_new_rounded, size: 15, color: SColors.textBody),
),
),
const Spacer(),
Text('1 of 7', style: SText.caption(SColors.textSub).copyWith(fontWeight: FontWeight.w600)),
],
),
);
}
Widget _nameInput() {
return Container(
decoration: BoxDecoration(
color: SColors.bgSurface,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: SColors.borderMedium, width: 1.5),
),
child: TextField(
controller: _ctrl,
autofocus: true,
textCapitalization: TextCapitalization.words,
style: SText.cardLabel(SColors.textHead).copyWith(fontSize: 18),
decoration: InputDecoration(
hintText: 'Your first name',
hintStyle: SText.cardLabel(SColors.textDisabled).copyWith(fontSize: 18, fontWeight: FontWeight.w500),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
),
),
);
}
}