redesign: duolingo-pattern onboarding, no gradient, pressable button
- Nunito font replacing Plus Jakarta Sans - SButton: Stack-based 3D pressable (dark shadow layer + top layer translates on press) - SCard: flat border-based selection, no shadows - SProgressBar: 4px thin bar, animated fill - All screens: white bg, no gradients - Duolingo layout: illustration top zone, content below, sticky button
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../theme.dart';
|
||||
import '../../widgets/primary_button.dart';
|
||||
import '../../widgets/quiz_option_row.dart';
|
||||
import '../../widgets/s_button.dart';
|
||||
import '../../widgets/s_card.dart';
|
||||
import '../../widgets/s_progress_bar.dart';
|
||||
import '../result_screen.dart';
|
||||
|
||||
class QuizData {
|
||||
@@ -34,18 +34,16 @@ class _QuizFlowState extends State<QuizFlow> {
|
||||
setState(() => _step++);
|
||||
_pageCtrl.animateToPage(
|
||||
_step,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
PageRouteBuilder(
|
||||
pageBuilder: (_, a, __) => ResultScreen(data: _data),
|
||||
transitionsBuilder: (_, a, __, child) =>
|
||||
FadeTransition(opacity: a, child: child),
|
||||
transitionDuration: const Duration(milliseconds: 500),
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pushReplacement(PageRouteBuilder(
|
||||
pageBuilder: (_, a, __) => ResultScreen(data: _data),
|
||||
transitionsBuilder: (_, a, __, child) =>
|
||||
FadeTransition(opacity: a, child: child),
|
||||
transitionDuration: const Duration(milliseconds: 400),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +52,7 @@ class _QuizFlowState extends State<QuizFlow> {
|
||||
setState(() => _step--);
|
||||
_pageCtrl.animateToPage(
|
||||
_step,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
duration: const Duration(milliseconds: 280),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
} else {
|
||||
@@ -62,16 +60,16 @@ class _QuizFlowState extends State<QuizFlow> {
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canProceed {
|
||||
return switch (_step) {
|
||||
0 => _data.name.trim().length >= 2,
|
||||
1 => _data.skinType.isNotEmpty,
|
||||
2 => _data.age.isNotEmpty,
|
||||
3 => _data.concerns.isNotEmpty,
|
||||
4 => _data.goal.isNotEmpty,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
bool get _canProceed => switch (_step) {
|
||||
0 => _data.name.trim().length >= 2,
|
||||
1 => _data.skinType.isNotEmpty,
|
||||
2 => _data.age.isNotEmpty,
|
||||
3 => _data.concerns.isNotEmpty,
|
||||
4 => _data.goal.isNotEmpty,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
String get _buttonLabel => _step == _totalSteps - 1 ? 'SEE MY RESULTS' : 'CONTINUE';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -83,12 +81,20 @@ class _QuizFlowState extends State<QuizFlow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: SColors.bgPage,
|
||||
backgroundColor: SColors.bgSurface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_topBar(),
|
||||
_progressBar(),
|
||||
// Nav row
|
||||
_navRow(),
|
||||
const SizedBox(height: 10),
|
||||
// Progress bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SProgressBar(value: (_step + 1) / _totalSteps),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Pages
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageCtrl,
|
||||
@@ -123,74 +129,57 @@ class _QuizFlowState extends State<QuizFlow> {
|
||||
],
|
||||
),
|
||||
),
|
||||
_bottomBar(),
|
||||
// Sticky bottom button
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||||
child: SButton(
|
||||
label: _buttonLabel,
|
||||
onTap: _canProceed ? _next : null,
|
||||
enabled: _canProceed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _topBar() {
|
||||
Widget _navRow() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back button
|
||||
GestureDetector(
|
||||
onTap: _back,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: SColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: SColors.border),
|
||||
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,
|
||||
),
|
||||
child: const Icon(Icons.arrow_back_ios_new_rounded, size: 16, color: SColors.textPrimary),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Step counter
|
||||
Text(
|
||||
'${_step + 1} of $_totalSteps',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SColors.textSecondary,
|
||||
),
|
||||
style: SText.caption(SColors.textSub).copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _progressBar() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: (_step + 1) / _totalSteps,
|
||||
backgroundColor: SColors.border,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(SColors.primary),
|
||||
minHeight: 4,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bottomBar() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 12, 24, 28),
|
||||
child: PrimaryButton(
|
||||
label: _step == _totalSteps - 1 ? 'See my results ✨' : 'Continue',
|
||||
icon: _step == _totalSteps - 1 ? null : Icons.arrow_forward_rounded,
|
||||
enabled: _canProceed,
|
||||
onTap: _next,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 1: Name ──────────────────────────────────────────────────────────────
|
||||
// ── Step: Name ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _NameStep extends StatelessWidget {
|
||||
final TextEditingController ctrl;
|
||||
@@ -201,68 +190,68 @@ class _NameStep extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Small illustration zone
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/images/shaynee_hero.png',
|
||||
height: 100,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 400.ms)
|
||||
.slideY(begin: 0.05, end: 0),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
Text(
|
||||
'Hey! What\'s\nyour name? 👋',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SColors.textPrimary,
|
||||
height: 1.2,
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms).slideY(begin: 0.1, end: 0),
|
||||
style: SText.headline2(SColors.textHead),
|
||||
).animate().fadeIn(duration: 350.ms),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'I\'ll personalise your routine just for you.',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 15,
|
||||
color: SColors.textSecondary,
|
||||
style: SText.subtitle(SColors.textSub),
|
||||
).animate().fadeIn(duration: 350.ms, delay: 80.ms),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
// Input — custom, no Material underline
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: SColors.bgSurface,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: SColors.borderMedium, width: 1.5),
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: 36),
|
||||
TextField(
|
||||
controller: ctrl,
|
||||
onChanged: onChanged,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: SColors.textPrimary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Your first name',
|
||||
hintStyle: GoogleFonts.plusJakartaSans(
|
||||
color: SColors.textSecondary,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: SColors.surface,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: SColors.border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: SColors.border, width: 1.5),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(color: SColors.primary, width: 2),
|
||||
child: TextField(
|
||||
controller: ctrl,
|
||||
onChanged: onChanged,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 200.ms),
|
||||
).animate().fadeIn(duration: 350.ms, delay: 160.ms),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 2: Skin Type ─────────────────────────────────────────────────────────
|
||||
// ── Step: Skin Type ───────────────────────────────────────────────────────────
|
||||
|
||||
class _SkinTypeStep extends StatelessWidget {
|
||||
final String selected;
|
||||
@@ -270,7 +259,7 @@ class _SkinTypeStep extends StatelessWidget {
|
||||
|
||||
const _SkinTypeStep({required this.selected, required this.onSelect});
|
||||
|
||||
static const _options = [
|
||||
static const _opts = [
|
||||
(Icons.water_drop_rounded, 'Oily', 'Shiny by midday, large pores'),
|
||||
(Icons.grass_rounded, 'Dry', 'Tight, flaky, rough patches'),
|
||||
(Icons.blur_on_rounded, 'Combination', 'Oily T-zone, dry cheeks'),
|
||||
@@ -281,37 +270,37 @@ class _SkinTypeStep extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'What\'s your\nskin type?',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SColors.textPrimary,
|
||||
height: 1.2,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('What\'s your\nskin type?', style: SText.headline2(SColors.textHead))
|
||||
.animate().fadeIn(duration: 350.ms),
|
||||
const SizedBox(height: 6),
|
||||
Text('Pick the one that fits best.', style: SText.subtitle(SColors.textSub))
|
||||
.animate().fadeIn(duration: 350.ms, delay: 80.ms),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Choose the one that best describes your skin.',
|
||||
style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: 24),
|
||||
...List.generate(_options.length, (i) {
|
||||
final (icon, label, sub) = _options[i];
|
||||
),
|
||||
..._opts.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final (icon, label, sub) = e.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: QuizOptionRow(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SCard(
|
||||
icon: icon,
|
||||
label: label,
|
||||
sub: sub,
|
||||
isSelected: selected == label,
|
||||
onTap: () => onSelect(label),
|
||||
).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms)
|
||||
.slideX(begin: 0.05, end: 0, duration: 300.ms),
|
||||
).animate().fadeIn(duration: 280.ms, delay: (80 + i * 50).ms)
|
||||
.slideX(begin: 0.03, end: 0, duration: 280.ms),
|
||||
);
|
||||
}),
|
||||
],
|
||||
@@ -320,7 +309,7 @@ class _SkinTypeStep extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 3: Age ───────────────────────────────────────────────────────────────
|
||||
// ── Step: Age ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class _AgeStep extends StatelessWidget {
|
||||
final String selected;
|
||||
@@ -328,7 +317,7 @@ class _AgeStep extends StatelessWidget {
|
||||
|
||||
const _AgeStep({required this.selected, required this.onSelect});
|
||||
|
||||
static const _options = [
|
||||
static const _opts = [
|
||||
(Icons.star_rounded, 'Under 25', 'Young & preventive care'),
|
||||
(Icons.hourglass_bottom_rounded, '25–35', 'Early anti-aging focus'),
|
||||
(Icons.wb_sunny_rounded, '35–45', 'Targeted treatments'),
|
||||
@@ -339,37 +328,38 @@ class _AgeStep extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'How old\nare you?',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SColors.textPrimary,
|
||||
height: 1.2,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('How old\nare you?', style: SText.headline2(SColors.textHead))
|
||||
.animate().fadeIn(duration: 350.ms),
|
||||
const SizedBox(height: 6),
|
||||
Text('Skin changes with age — helps us tailor advice.',
|
||||
style: SText.subtitle(SColors.textSub))
|
||||
.animate().fadeIn(duration: 350.ms, delay: 80.ms),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Skin changes with age — this helps us tailor advice.',
|
||||
style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: 24),
|
||||
...List.generate(_options.length, (i) {
|
||||
final (icon, label, sub) = _options[i];
|
||||
),
|
||||
..._opts.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final (icon, label, sub) = e.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: QuizOptionRow(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SCard(
|
||||
icon: icon,
|
||||
label: label,
|
||||
sub: sub,
|
||||
isSelected: selected == label,
|
||||
onTap: () => onSelect(label),
|
||||
).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms)
|
||||
.slideX(begin: 0.05, end: 0, duration: 300.ms),
|
||||
).animate().fadeIn(duration: 280.ms, delay: (80 + i * 50).ms)
|
||||
.slideX(begin: 0.03, end: 0, duration: 280.ms),
|
||||
);
|
||||
}),
|
||||
],
|
||||
@@ -378,7 +368,7 @@ class _AgeStep extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4: Concerns ──────────────────────────────────────────────────────────
|
||||
// ── Step: Concerns ────────────────────────────────────────────────────────────
|
||||
|
||||
class _ConcernsStep extends StatelessWidget {
|
||||
final List<String> selected;
|
||||
@@ -386,7 +376,7 @@ class _ConcernsStep extends StatelessWidget {
|
||||
|
||||
const _ConcernsStep({required this.selected, required this.onToggle});
|
||||
|
||||
static const _options = [
|
||||
static const _opts = [
|
||||
(Icons.face_retouching_natural, 'Breakouts', 'Acne, blackheads, congestion'),
|
||||
(Icons.cloud_rounded, 'Dullness', 'Tired, uneven, grey skin'),
|
||||
(Icons.air_rounded, 'Dryness', 'Tight, rough, flaking'),
|
||||
@@ -398,42 +388,61 @@ class _ConcernsStep extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your skin\nconcerns?',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SColors.textPrimary,
|
||||
height: 1.2,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text('Your skin concerns?', style: SText.headline2(SColors.textHead))
|
||||
.animate().fadeIn(duration: 350.ms),
|
||||
const Spacer(),
|
||||
// counter badge
|
||||
if (selected.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: SColors.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${selected.length}/3',
|
||||
style: SText.caption(Colors.white)
|
||||
.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text('Pick up to 3 that bother you most.', style: SText.subtitle(SColors.textSub))
|
||||
.animate().fadeIn(duration: 350.ms, delay: 80.ms),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pick up to 3 that bother you most.',
|
||||
style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: 24),
|
||||
...List.generate(_options.length, (i) {
|
||||
final (icon, label, sub) = _options[i];
|
||||
final isSelected = selected.contains(label);
|
||||
final disabled = !isSelected && selected.length >= 3;
|
||||
),
|
||||
..._opts.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final (icon, label, sub) = e.value;
|
||||
final isSel = selected.contains(label);
|
||||
final disabled = !isSel && selected.length >= 3;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Opacity(
|
||||
opacity: disabled ? 0.4 : 1.0,
|
||||
child: QuizOptionRow(
|
||||
opacity: disabled ? 0.35 : 1.0,
|
||||
child: SCard(
|
||||
icon: icon,
|
||||
label: label,
|
||||
sub: sub,
|
||||
isSelected: isSelected,
|
||||
isSelected: isSel,
|
||||
onTap: disabled ? () {} : () => onToggle(label),
|
||||
),
|
||||
).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms)
|
||||
.slideX(begin: 0.05, end: 0, duration: 300.ms),
|
||||
).animate().fadeIn(duration: 280.ms, delay: (80 + i * 50).ms)
|
||||
.slideX(begin: 0.03, end: 0, duration: 280.ms),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
@@ -442,7 +451,7 @@ class _ConcernsStep extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 5: Goal ──────────────────────────────────────────────────────────────
|
||||
// ── Step: Goal ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _GoalStep extends StatelessWidget {
|
||||
final String selected;
|
||||
@@ -450,7 +459,7 @@ class _GoalStep extends StatelessWidget {
|
||||
|
||||
const _GoalStep({required this.selected, required this.onSelect});
|
||||
|
||||
static const _options = [
|
||||
static const _opts = [
|
||||
(Icons.auto_awesome_rounded, 'Healthy glow', 'Radiant, dewy, lit-from-within'),
|
||||
(Icons.shield_rounded, 'Clear skin', 'Blemish-free, smooth texture'),
|
||||
(Icons.access_time_rounded, 'Slow aging', 'Firm, youthful, preventive'),
|
||||
@@ -460,37 +469,38 @@ class _GoalStep extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(24, 32, 24, 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Your skin\ngoal?',
|
||||
style: GoogleFonts.plusJakartaSans(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: SColors.textPrimary,
|
||||
height: 1.2,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Your main\nskin goal?', style: SText.headline2(SColors.textHead))
|
||||
.animate().fadeIn(duration: 350.ms),
|
||||
const SizedBox(height: 6),
|
||||
Text('We\'ll build your plan around it.',
|
||||
style: SText.subtitle(SColors.textSub))
|
||||
.animate().fadeIn(duration: 350.ms, delay: 80.ms),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(duration: 400.ms),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Pick one — we\'ll build your plan around it.',
|
||||
style: GoogleFonts.plusJakartaSans(fontSize: 15, color: SColors.textSecondary),
|
||||
).animate().fadeIn(duration: 400.ms, delay: 100.ms),
|
||||
const SizedBox(height: 24),
|
||||
...List.generate(_options.length, (i) {
|
||||
final (icon, label, sub) = _options[i];
|
||||
),
|
||||
..._opts.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final (icon, label, sub) = e.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: QuizOptionRow(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: SCard(
|
||||
icon: icon,
|
||||
label: label,
|
||||
sub: sub,
|
||||
isSelected: selected == label,
|
||||
onTap: () => onSelect(label),
|
||||
).animate().fadeIn(duration: 300.ms, delay: (100 + i * 60).ms)
|
||||
.slideX(begin: 0.05, end: 0, duration: 300.ms),
|
||||
).animate().fadeIn(duration: 280.ms, delay: (80 + i * 50).ms)
|
||||
.slideX(begin: 0.03, end: 0, duration: 280.ms),
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user