diff --git a/lib/home_screen.dart b/lib/home_screen.dart index f80a2ed..1033778 100644 --- a/lib/home_screen.dart +++ b/lib/home_screen.dart @@ -33,10 +33,7 @@ class _HomeScreenState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFE6F1), - Color(0xFFFFC9DF), - ], + colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], ), ), ), @@ -64,22 +61,67 @@ class _HomeScreenState extends State { ), Center( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.symmetric(horizontal: 28), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color( + 0xFF2F9E94, + ).withValues(alpha: 0.18), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: const Icon( + Icons.medical_services_rounded, + size: 38, + color: Color(0xFF2F9E94), + ), + ), + const SizedBox(height: 18), const Text( 'Check-Teeth Kids', textAlign: TextAlign.center, style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.w800, + fontSize: 28, + fontWeight: FontWeight.w900, color: Color(0xFFFF55A7), height: 1.0, + letterSpacing: -0.5, ), ), - const SizedBox(height: 22), + const SizedBox(height: 10), + Text( + 'Cuidar do sorriso começa aqui.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: const Color(0xFF2F9E94).withValues(alpha: 0.9), + ), + ), + const SizedBox(height: 6), + Text( + 'Acompanhe a saúde oral do seu filho com\ninformação segura e prevenção inteligente.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + height: 1.35, + color: Colors.black.withValues(alpha: 0.52), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 32), SizedBox( width: size.width * 0.78, child: _PrimaryButton( @@ -87,39 +129,14 @@ class _HomeScreenState extends State { onPressed: _openRegister, ), ), - const SizedBox(height: 14), + const SizedBox(height: 12), SizedBox( width: size.width * 0.78, - child: _PrimaryButton( + child: _SecondaryButton( label: 'Entrar', onPressed: _openLogin, ), ), - const SizedBox(height: 24), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle( - fontSize: 14.0, - height: 1.25, - color: Colors.black.withValues(alpha: 0.55), - fontWeight: FontWeight.w600, - ), - children: const [ - TextSpan( - text: 'Cuidar do sorriso começa aqui.\n', - style: TextStyle( - color: Color(0xFF2F9E94), - fontWeight: FontWeight.w900, - ), - ), - TextSpan( - text: - 'Acompanhe a saúde oral do seu filho com\ninformação segura e prevenção inteligente.', - ), - ], - ), - ), ], ), ), @@ -150,6 +167,32 @@ class _HomeScreenState extends State { } } +class _SecondaryButton extends StatelessWidget { + const _SecondaryButton({required this.label, required this.onPressed}); + + final String label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + const Color teal = Color(0xFF2F9E94); + return SizedBox( + height: 44, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: teal, + side: const BorderSide(color: teal, width: 1.6), + shape: const StadiumBorder(), + backgroundColor: Colors.white.withValues(alpha: 0.5), + textStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15), + ), + onPressed: onPressed, + child: Text(label), + ), + ); + } +} + class _PrimaryButton extends StatelessWidget { const _PrimaryButton({required this.label, required this.onPressed}); @@ -162,26 +205,29 @@ class _PrimaryButton extends StatelessWidget { return SizedBox( height: 44, child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: teal, - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 15), - ).copyWith( - animationDuration: const Duration(milliseconds: 180), - splashFactory: InkSparkle.splashFactory, - overlayColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.pressed)) { - return Colors.white.withValues(alpha: 0.14); - } - if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { - return Colors.white.withValues(alpha: 0.08); - } - return null; - }, - ), - ), + style: + FilledButton.styleFrom( + backgroundColor: teal, + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 15, + ), + ).copyWith( + animationDuration: const Duration(milliseconds: 180), + splashFactory: InkSparkle.splashFactory, + overlayColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return Colors.white.withValues(alpha: 0.14); + } + if (states.contains(WidgetState.hovered) || + states.contains(WidgetState.focused)) { + return Colors.white.withValues(alpha: 0.08); + } + return null; + }), + ), onPressed: onPressed, child: Text(label), ), diff --git a/lib/logged_home.dart b/lib/logged_home.dart index 77807e8..5971b62 100644 --- a/lib/logged_home.dart +++ b/lib/logged_home.dart @@ -12,7 +12,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'quiz/quiz1.dart'; import 'quiz/quiz_prefs.dart'; -import 'screens/curiosidade_screen.dart'; import 'screens/video_screen.dart'; class LoggedHomeScreen extends StatefulWidget { @@ -205,11 +204,7 @@ class _LoggedHomeScreenState extends State ? _expandedAppBarHeight : _collapsedAppBarHeight; final double toolbarHeight = _index == 0 ? kToolbarHeight : appBarHeight; - final String title = _index == 0 - ? '' - : _index == 1 - ? 'Perfil' - : 'Configurações'; + final String title = _index == 0 ? '' : 'Perfil'; final ShapeBorder appBarShape = _index == 0 ? const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)), @@ -290,43 +285,9 @@ class _LoggedHomeScreenState extends State Positioned( left: 0, right: 0, - bottom: 24, + bottom: 12, child: Center( - child: TweenAnimationBuilder( - duration: const Duration(milliseconds: 520), - curve: Curves.easeOutCubic, - tween: Tween( - begin: 0, - end: percent / 100.0, - ), - builder: (context, value, _) { - final shown = (value * 100).round(); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withValues( - alpha: 0.18, - ), - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: Colors.white.withValues( - alpha: 0.22, - ), - ), - ), - child: Text( - '$shown%', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - ); - }, - ), + child: _RiskArcGauge(percent: percent), ), ), ], @@ -398,8 +359,7 @@ class _LoggedHomeScreenState extends State padding: EdgeInsets.fromLTRB(16, bodyTopPadding, 16, 16), child: _index == 0 ? _InicioTab(onQuizClosed: _loadQuizResult) - : _index == 1 - ? _PerfilTab( + : _PerfilTab( selectedChildIndex: _selectedChildIndex, onChildSelected: (index, name, scopeId) { setState(() { @@ -409,8 +369,7 @@ class _LoggedHomeScreenState extends State }); _loadQuizResult(); }, - ) - : const _ConfigTab(), + ), ), ), ), @@ -432,16 +391,112 @@ class _LoggedHomeScreenState extends State icon: Icon(Icons.person_rounded), label: 'Perfil', ), - BottomNavigationBarItem( - icon: Icon(Icons.settings_rounded), - label: 'Config.', - ), ], ), ); } } +class _RiskArcGauge extends StatelessWidget { + const _RiskArcGauge({required this.percent}); + + final int percent; + + @override + Widget build(BuildContext context) { + final clamped = percent.clamp(0, 100); + + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 700), + curve: Curves.easeOutCubic, + tween: Tween(begin: 0, end: clamped / 100), + builder: (context, value, _) { + final shown = (value * 100).round(); + return SizedBox( + width: 178, + height: 94, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: CustomPaint( + painter: _RiskArcGaugePainter(progress: value), + ), + ), + Positioned( + bottom: 8, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$shown%', + style: const TextStyle( + color: Colors.white, + fontSize: 26, + fontWeight: FontWeight.w900, + height: 1, + ), + ), + const SizedBox(height: 4), + Text( + 'Risco de Má Oclusão', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.92), + fontSize: 9, + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _RiskArcGaugePainter extends CustomPainter { + const _RiskArcGaugePainter({required this.progress}); + + final double progress; + + @override + void paint(Canvas canvas, Size size) { + final rect = Rect.fromLTWH(16, 12, size.width - 32, size.height * 1.55); + const startAngle = math.pi; + const sweepAngle = math.pi; + final strokeWidth = size.width * 0.14; + + final backgroundPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.72) + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.butt; + + final progressPaint = Paint() + ..color = const Color(0xFFFF9AD0) + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.butt; + + canvas.drawArc(rect, startAngle, sweepAngle, false, backgroundPaint); + canvas.drawArc( + rect, + startAngle, + sweepAngle * progress.clamp(0, 1), + false, + progressPaint, + ); + } + + @override + bool shouldRepaint(covariant _RiskArcGaugePainter oldDelegate) { + return oldDelegate.progress != progress; + } +} + class _InicioTab extends StatelessWidget { const _InicioTab({required this.onQuizClosed}); @@ -455,6 +510,7 @@ class _InicioTab extends StatelessWidget { final scopeId = childScope.isNotEmpty ? childScope : (uid.isNotEmpty ? uid : null); + final selectedChildName = (state?._selectedChildName ?? '').trim(); return Align( alignment: Alignment.topCenter, @@ -462,13 +518,14 @@ class _InicioTab extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 560), child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.only(top: 18, bottom: 10), + padding: const EdgeInsets.only(top: 18, bottom: 16), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _MenuButton( - label: 'Iniciar Quiz', - onPressed: () { + _HeroQuizCard( + childName: selectedChildName, + onStartQuiz: () { Navigator.of(context) .push( MaterialPageRoute( @@ -478,7 +535,7 @@ class _InicioTab extends StatelessWidget { .then((_) => onQuizClosed()); }, ), - const SizedBox(height: 14), + const SizedBox(height: 16), _VideoLibraryCard( onOpenLibrary: () { Navigator.of(context).push( @@ -488,19 +545,7 @@ class _InicioTab extends StatelessWidget { ); }, ), - const SizedBox(height: 14), - _MenuButton( - label: 'Curiosidades', - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const CuriosidadeScreen(), - ), - ); - }, - ), - const SizedBox(height: 14), - const _ClinicsSection(), + const SizedBox(height: 16), ], ), ), @@ -510,325 +555,96 @@ class _InicioTab extends StatelessWidget { } } -class ClinicItem { - const ClinicItem({ - required this.name, - required this.subtitle, - required this.rating, - required this.description, - required this.address, - required this.phone, - }); +class _HeroQuizCard extends StatelessWidget { + const _HeroQuizCard({required this.childName, required this.onStartQuiz}); - final String name; - final String subtitle; - final double rating; - final String description; - final String address; - final String phone; -} - -class _ClinicsSection extends StatelessWidget { - const _ClinicsSection(); - - static const List _clinics = [ - ClinicItem( - name: 'PóvaMed', - subtitle: 'Clínica Médica e Dentária', - rating: 4.9, - description: - 'Atendimento odontopediátrico e familiar. Agende e tire dúvidas pelo telefone.', - address: 'R. Patrão Lagoa 12, 4490-578 Póvoa de Varzim', - phone: '+351 000 000 000', - ), - ClinicItem( - name: 'ORTO-M', - subtitle: 'Ortodontia e Implantes', - rating: 3.8, - description: - 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', - address: 'R. de 31 de Janeiro, 4490-533 Póvoa de Varzim', - phone: '+351 000 000 001', - ), - ClinicItem( - name: 'Dentart', - subtitle: 'Clínica Médica E Dentária', - rating: 5.0, - description: - 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', - address: 'Rua Ramalho Ortigão 198 R/C, 4490-678 Póvoa De Varzim', - phone: '+351 000 000 001', - ), - ClinicItem( - name: 'S. Cipriano', - subtitle: 'Clínica Médico-Cirúrgica', - rating: 3.0, - description: - 'Avaliação inicial e planos de ortodontia. Atendimento por marcação.', - address: 'Praça do Almada 7 2º, 4490-438 Póvoa De Varzim', - phone: '+351 000 000 001', - ), - ]; + final String childName; + final VoidCallback onStartQuiz; @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); + const Color teal = Color(0xFF2F9E94); + const Color pink = Color(0xFFFF55A7); - return LayoutBuilder( - builder: (context, constraints) { - final double cardWidth = math.min( - constraints.maxWidth, - size.width * 0.78, - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 8, top: 8), - child: Text( - 'Clínicas Parceiras', - style: TextStyle( - color: Color(0xFF2F9E94), - fontWeight: FontWeight.w900, - fontSize: 16, - ), - ), - ), - Container( - height: 2.6, - decoration: BoxDecoration( - color: const Color(0xFF2F9E94).withValues(alpha: 0.85), - borderRadius: BorderRadius.circular(999), - ), - ), - const SizedBox(height: 12), - for (final clinic in _clinics) ...[ - Center( - child: SizedBox( - width: cardWidth, - child: _ClinicCard(item: clinic), - ), - ), - const SizedBox(height: 12), - ], - ], - ); - }, - ); - } -} - -class _ClinicCard extends StatelessWidget { - const _ClinicCard({required this.item}); - - final ClinicItem item; - - @override - Widget build(BuildContext context) { return Material( - color: const Color(0xFFFF55A7), - borderRadius: BorderRadius.circular(22), - elevation: 10, + elevation: 12, shadowColor: Colors.black.withValues(alpha: 0.18), - child: InkWell( - borderRadius: BorderRadius.circular(22), - onTap: () { - showModalBottomSheet( - context: context, - showDragHandle: true, - backgroundColor: const Color(0xFFFFE6F1), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - builder: (ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.fromLTRB(18, 6, 18, 18), + borderRadius: BorderRadius.circular(24), + color: Colors.white, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: teal.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon( + Icons.medical_services_rounded, + color: teal, + ), + ), + const SizedBox(width: 12), + Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - item.name, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), - ), - ), - const SizedBox(height: 6), - Text( - item.subtitle, - textAlign: TextAlign.center, + const Text( + 'Avaliação de saúde oral', style: TextStyle( - color: Colors.black.withValues(alpha: 0.72), - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w900, + fontSize: 16, + color: pink, ), ), - const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.82), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.black.withValues(alpha: 0.08), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _ClinicInfoRow( - label: 'Avaliação', - value: item.rating.toStringAsFixed(1), - ), - const SizedBox(height: 10), - _ClinicInfoRow( - label: 'Endereço', - value: item.address, - ), - const SizedBox(height: 10), - _ClinicInfoRow(label: 'Contato', value: item.phone), - const SizedBox(height: 12), - Text( - item.description, - style: TextStyle( - color: Colors.black.withValues(alpha: 0.72), - fontWeight: FontWeight.w600, - height: 1.25, - ), - ), - ], + const SizedBox(height: 2), + Text( + childName.isNotEmpty + ? 'Para $childName' + : 'Responda o quiz e descubra como cuidar melhor do sorriso.', + style: TextStyle( + color: Colors.black.withValues(alpha: 0.62), + fontWeight: FontWeight.w600, + fontSize: 13, ), ), ], ), ), - ); - }, - ); - }, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 16, - ), - ), - const SizedBox(height: 2), - Text( - item.subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.90), - fontWeight: FontWeight.w700, - fontSize: 12, - ), - ), - const SizedBox(height: 6), - Text( - item.address, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.88), - fontWeight: FontWeight.w700, - fontSize: 11, - ), - ), - ], - ), - ), - const SizedBox(width: 10), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.18), - borderRadius: BorderRadius.circular(999), - border: Border.all( - color: Colors.white.withValues(alpha: 0.22), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 48, + width: double.infinity, + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: pink, + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontWeight: FontWeight.w800, + fontSize: 15, ), ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.star_rounded, - color: Colors.white, - size: 18, - ), - const SizedBox(width: 4), - Text( - item.rating.toStringAsFixed(1), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - ), - ), - ], - ), + onPressed: onStartQuiz, + icon: const Icon(Icons.play_arrow_rounded), + label: const Text('Iniciar Quiz'), ), - ], - ), + ), + ], ), ), ); } } -class _ClinicInfoRow extends StatelessWidget { - const _ClinicInfoRow({required this.label, required this.value}); - - final String label; - final String value; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 86, - child: Text( - label, - style: const TextStyle( - color: Color(0xFF2F9E94), - fontWeight: FontWeight.w900, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - value, - style: TextStyle( - color: Colors.black.withValues(alpha: 0.74), - fontWeight: FontWeight.w700, - ), - ), - ), - ], - ); - } -} - class _VideoLibraryCard extends StatelessWidget { const _VideoLibraryCard({required this.onOpenLibrary}); @@ -836,133 +652,115 @@ class _VideoLibraryCard extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); final item = VideoScreen.library.isEmpty ? null : VideoScreen.library.first; - return SizedBox( - width: size.width * 0.78, - child: Material( - color: const Color(0xFFFF55A7), - borderRadius: BorderRadius.circular(22), - elevation: 10, - shadowColor: Colors.black.withValues(alpha: 0.18), - child: InkWell( - borderRadius: BorderRadius.circular(22), - onTap: onOpenLibrary, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - const Expanded( - child: Text( - 'Vídeos Educativos', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 16, + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + elevation: 12, + shadowColor: Colors.black.withValues(alpha: 0.18), + child: InkWell( + borderRadius: BorderRadius.circular(24), + onTap: onOpenLibrary, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (item == null) + Container( + height: 160, + decoration: BoxDecoration( + color: const Color(0xFFFFE6F1), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + ) + else + ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + child: SizedBox( + height: 160, + child: Stack( + fit: StackFit.expand, + children: [ + _VideoThumbnail(url: item.url), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withValues(alpha: 0.05), + Colors.black.withValues(alpha: 0.45), + ], + ), ), ), - ), - Icon( - Icons.chevron_right_rounded, - color: Colors.white.withValues(alpha: 0.95), - size: 26, - ), - ], - ), - const SizedBox(height: 14), - if (item == null) - Container( - height: 140, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.18), - borderRadius: BorderRadius.circular(18), - ), - ) - else - _VideoLargePreview( - title: item.title, - url: item.url, - onTap: onOpenLibrary, + const Align( + alignment: Alignment.center, + child: Icon( + Icons.play_circle_fill_rounded, + size: 54, + color: Colors.white, + ), + ), + ], ), - ], - ), - ), - ), - ), - ); - } -} - -class _VideoLargePreview extends StatelessWidget { - const _VideoLargePreview({ - required this.title, - required this.url, - required this.onTap, - }); - - final String title; - final String url; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Material( - color: Colors.white.withValues(alpha: 0.22), - borderRadius: BorderRadius.circular(18), - child: InkWell( - borderRadius: BorderRadius.circular(18), - onTap: onTap, - child: SizedBox( - height: 140, - child: ClipRRect( - borderRadius: BorderRadius.circular(18), - child: Stack( - fit: StackFit.expand, - children: [ - _VideoThumbnail(url: url), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.black.withValues(alpha: 0.10), - Colors.black.withValues(alpha: 0.58), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(18, 14, 14, 14), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: const Color(0xFF2F9E94).withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.video_library_rounded, + color: Color(0xFF2F9E94), + size: 20, + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Vídeos Educativos', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 15, + color: Color(0xFFFF55A7), + ), + ), + SizedBox(height: 2), + Text( + 'Aprenda mais sobre saúde oral', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.black54, + ), + ), ], ), ), - ), - const Align( - alignment: Alignment.center, - child: Icon( - Icons.play_circle_fill_rounded, - size: 58, - color: Colors.white, + const Icon( + Icons.chevron_right_rounded, + color: Color(0xFF2F9E94), + size: 24, ), - ), - Align( - alignment: Alignment.bottomLeft, - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 10, 14, 14), - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 14, - ), - ), - ), - ), - ], + ], + ), ), - ), + ], ), ), ); @@ -1201,6 +999,7 @@ class _PerfilTabState extends State<_PerfilTab> { final user = FirebaseAuth.instance.currentUser; final uid = (user?.uid ?? '').trim(); final name = (user?.displayName ?? '').trim(); + final email = (user?.email ?? '').trim(); final shownName = name.isNotEmpty ? name : 'Sem nome'; if (uid.isEmpty) { @@ -1217,6 +1016,8 @@ class _PerfilTabState extends State<_PerfilTab> { final storedName = (data?['name'] ?? '').toString().trim(); final profileName = storedName.isNotEmpty ? storedName : shownName; final photoUrl = (data?['photoUrl'] ?? '').toString().trim(); + final storedEmail = (data?['email'] ?? '').toString().trim(); + final profileEmail = storedEmail.isNotEmpty ? storedEmail : email; return StreamBuilder>>( stream: FirebaseFirestore.instance @@ -1249,79 +1050,205 @@ class _PerfilTabState extends State<_PerfilTab> { elevation: 10, color: Colors.white, borderRadius: BorderRadius.circular(20), + shadowColor: Colors.black.withValues(alpha: 0.16), child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(18), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ InkWell( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(40), onTap: _updatingPhoto ? null : () => _pickAndUploadProfilePhoto( context, uid, ), - child: Container( - width: 72, - height: 72, - decoration: BoxDecoration( - color: const Color(0xFFFFE6F1), - borderRadius: BorderRadius.circular(16), - ), - clipBehavior: Clip.antiAlias, - child: Stack( - fit: StackFit.expand, - children: [ - if (photoUrl.isNotEmpty) - Image.network( - photoUrl, - fit: BoxFit.cover, - ) - else - const Icon( - Icons.person_rounded, - size: 40, - color: Color(0xFF2F9E94), + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 76, + height: 76, + decoration: BoxDecoration( + color: const Color(0xFFFFE6F1), + shape: BoxShape.circle, + border: Border.all( + color: const Color( + 0xFF2F9E94, + ).withValues(alpha: 0.35), + width: 2, ), - if (_updatingPhoto) - Container( - color: Colors.black.withValues( - alpha: 0.25, - ), - child: const Center( - child: SizedBox( - width: 22, - height: 22, - child: - CircularProgressIndicator( - strokeWidth: 2, - ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + if (photoUrl.isNotEmpty) + Image.network( + photoUrl, + fit: BoxFit.cover, + ) + else + const Icon( + Icons.person_rounded, + size: 42, + color: Color(0xFF2F9E94), ), - ), + if (_updatingPhoto) + Container( + color: Colors.black.withValues( + alpha: 0.25, + ), + child: const Center( + child: SizedBox( + width: 22, + height: 22, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ), + ), + ], + ), + ), + Positioned( + right: -2, + bottom: -2, + child: Container( + width: 26, + height: 26, + decoration: const BoxDecoration( + color: Color(0xFFFF55A7), + shape: BoxShape.circle, ), - ], - ), + child: const Icon( + Icons.camera_alt_rounded, + size: 14, + color: Colors.white, + ), + ), + ), + ], ), ), - const SizedBox(width: 12), + const SizedBox(width: 14), Expanded( - child: Text( - profileName, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w900, - color: Color(0xFFFF55A7), - ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + profileName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: Color(0xFFFF55A7), + ), + ), + if (profileEmail.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + profileEmail, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.black.withValues( + alpha: 0.55, + ), + ), + ), + ], + ], ), ), ], ), ), ), - const SizedBox(height: 14), + const SizedBox(height: 22), + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 10), + child: Row( + children: [ + const Text( + 'Meus filhos', + style: TextStyle( + color: Color(0xFF2F9E94), + fontWeight: FontWeight.w900, + fontSize: 15, + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: const Color( + 0xFF2F9E94, + ).withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + '${children.length}', + style: const TextStyle( + color: Color(0xFF2F9E94), + fontWeight: FontWeight.w900, + fontSize: 12, + ), + ), + ), + ], + ), + ), if (children.isEmpty) - const SizedBox.shrink() + Container( + padding: const EdgeInsets.all(18), + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.black.withValues(alpha: 0.08), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFFFE6F1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.child_care_rounded, + color: Color(0xFFFF55A7), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Nenhuma criança adicionada ainda.', + style: TextStyle( + color: Colors.black.withValues( + alpha: 0.62, + ), + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ) else ...children.asMap().entries.map((entry) { final i = entry.key; @@ -1447,8 +1374,8 @@ class _PerfilTabState extends State<_PerfilTab> { ); }), SizedBox( - height: 46, - child: FilledButton( + height: 48, + child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF2F9E94), foregroundColor: Colors.white, @@ -1460,9 +1387,33 @@ class _PerfilTabState extends State<_PerfilTab> { onPressed: _addingChild ? null : () => _addAnotherChild(context, uid), - child: const Text('Adicionar outra criança'), + icon: const Icon(Icons.add_rounded), + label: const Text('Adicionar criança'), ), ), + const SizedBox(height: 22), + SizedBox( + height: 46, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFFF55A7), + side: const BorderSide( + color: Color(0xFFFF55A7), + width: 1.4, + ), + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontWeight: FontWeight.w800, + ), + ), + onPressed: () async { + await FirebaseAuth.instance.signOut(); + }, + icon: const Icon(Icons.logout_rounded), + label: const Text('Sair'), + ), + ), + const SizedBox(height: 12), ], ), ), @@ -1733,100 +1684,3 @@ class _AddChildSheetState extends State<_AddChildSheet> { ); } } - -class _ConfigTab extends StatelessWidget { - const _ConfigTab(); - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - final Color accentPink = const Color(0xFFFF55A7); - - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: size.width * 0.60, - height: 44, - child: FilledButton( - style: - FilledButton.styleFrom( - backgroundColor: accentPink, - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle( - fontWeight: FontWeight.w800, - fontSize: 15, - ), - ).copyWith( - animationDuration: const Duration(milliseconds: 180), - splashFactory: InkSparkle.splashFactory, - overlayColor: WidgetStateProperty.resolveWith(( - states, - ) { - if (states.contains(WidgetState.pressed)) { - return Colors.white.withValues(alpha: 0.14); - } - if (states.contains(WidgetState.hovered) || - states.contains(WidgetState.focused)) { - return Colors.white.withValues(alpha: 0.08); - } - return null; - }), - ), - onPressed: () async { - await FirebaseAuth.instance.signOut(); - }, - child: const Text('Sair'), - ), - ), - ], - ); - } -} - -class _MenuButton extends StatelessWidget { - const _MenuButton({required this.label, required this.onPressed}); - - final String label; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - const Color accentPink = Color(0xFFFF55A7); - - return SizedBox( - width: size.width * 0.78, - height: 50, - child: FilledButton( - style: - FilledButton.styleFrom( - backgroundColor: accentPink, - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle( - fontWeight: FontWeight.w800, - fontSize: 15, - ), - ).copyWith( - animationDuration: const Duration(milliseconds: 180), - splashFactory: InkSparkle.splashFactory, - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.white.withValues(alpha: 0.14); - } - if (states.contains(WidgetState.hovered) || - states.contains(WidgetState.focused)) { - return Colors.white.withValues(alpha: 0.08); - } - return null; - }), - ), - onPressed: onPressed, - child: Text(label, textAlign: TextAlign.center), - ), - ); - } -} diff --git a/lib/quiz/quiz1.dart b/lib/quiz/quiz1.dart index 3bd6e43..de88520 100644 --- a/lib/quiz/quiz1.dart +++ b/lib/quiz/quiz1.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'quiz_question_screen.dart'; import 'quiz_result.dart'; -// Quiz 1: Tipos de Escova (antiga Quiz 6) +// Quiz 1: Face (Image-based) class Quiz1Screen extends StatelessWidget { const Quiz1Screen({super.key, this.currentScore = 0, this.scopeId}); @@ -13,38 +13,50 @@ class Quiz1Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 1/20', - question: 'Qual tipo de escova é mais recomendada para crianças?', + title: 'Quiz 1/26', + question: + 'Qual das seguintes imagens se assemelha à face do seu filho/a?', answers: const [ QuizAnswer( - title: 'Escova macia com cabeça pequena', + title: 'Opção A', description: - 'Escovas macias protegem a gengiva sensível das crianças e a cabeça pequena alcança melhor todos os dentes.', + 'Selecione se a imagem se assemelha à face do seu filho/a', weight: 2, + imagePath: 'assets/images/face_a.png', ), QuizAnswer( - title: 'Escova dura para limpar melhor', + title: 'Opção B', description: - 'Escovas duras podem machucar a gengiva e desgastar o esmalte dos dentes das crianças.', - weight: 5, + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_b.png', ), QuizAnswer( - title: 'Escova elétrica sempre é melhor', + title: 'Opção C', description: - 'Escova elétrica pode ajudar, mas não é essencial. O mais importante é a técnica e frequência.', - weight: 3, + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: false, ); } } -// Quiz 2: Alimentos que Causam Cáries (antiga Quiz 7) +// Quiz 2: Boca (Image-based) class Quiz2Screen extends StatelessWidget { const Quiz2Screen({super.key, required this.currentScore, this.scopeId}); @@ -54,38 +66,50 @@ class Quiz2Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 2/20', - question: 'Qual alimento é mais prejudicial para os dentes?', + title: 'Quiz 2/26', + question: + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', answers: const [ QuizAnswer( - title: 'Balas e chicletes pegajosos', + title: 'Opção A', description: - 'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.', + 'Selecione se a imagem se assemelha à boca do seu filho/a', weight: 2, + imagePath: 'assets/images/mouth_a.png', ), QuizAnswer( - title: 'Frutas frescas', + title: 'Opção B', description: - 'Frutas são saudáveis, mas algumas são ácidas. O problema maior são os alimentos açucarados e pegajosos.', - weight: 5, + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_b.png', ), QuizAnswer( - title: 'Vegetais crus', + title: 'Opção C', description: - 'Vegetais são geralmente seguros para os dentes e muitos ajudam na limpeza natural.', - weight: 3, + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: true, ); } } -// Quiz 3: Primeira Visita ao Dentista (antiga Quiz 8) +// Quiz 3: Olheiras (Image-based) class Quiz3Screen extends StatelessWidget { const Quiz3Screen({super.key, required this.currentScore, this.scopeId}); @@ -95,38 +119,50 @@ class Quiz3Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 3/20', - question: 'Qual a idade ideal para a primeira visita ao dentista?', + title: 'Quiz 3/26', + question: + 'Qual das seguintes imagens se assemelha às olheiras do seu filho/a?', answers: const [ QuizAnswer( - title: 'A partir dos 1 ano de idade', + title: 'Opção A', description: - 'O recomendado é levar ao dentista assim que o primeiro dentição aparecer ou até 1 ano.', + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', weight: 2, + imagePath: 'assets/images/dark_circles_a.png', ), QuizAnswer( - title: 'Só depois dos 6 anos', + title: 'Opção B', description: - 'Esperar demais pode permitir que problemas sérios se desenvolvam sem detecção precoce.', - weight: 5, + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_b.png', ), QuizAnswer( - title: 'Apenas quando houver dor', + title: 'Opção C', description: - 'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.', - weight: 4, + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: true, ); } } -// Quiz 4: Uso de Fio Dental (antiga Quiz 9) +// Quiz 4: Queixo (Image-based) class Quiz4Screen extends StatelessWidget { const Quiz4Screen({super.key, required this.currentScore, this.scopeId}); @@ -136,38 +172,50 @@ class Quiz4Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 4/20', - question: 'Com que frequência crianças devem usar fio dental?', + title: 'Quiz 4/26', + question: + 'Qual das seguintes imagens se assemelha ao queixo do seu filho/a com a boca fechada?', answers: const [ QuizAnswer( - title: 'Pelo menos uma vez ao dia', + title: 'Opção A', description: - 'O uso diário de fio dental é importante para remover placa entre os dentes onde a escova não alcança.', + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', weight: 2, + imagePath: 'assets/images/chin_a.png', ), QuizAnswer( - title: 'Só quando os dentes estiverem muito juntos', + title: 'Opção B', description: - 'Fio dental é necessário independentemente do espaçamento dos dentes para remover placa bacteriana.', - weight: 5, + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_b.png', ), QuizAnswer( - title: 'Uma vez por semana é suficiente', + title: 'Opção C', description: - 'Uso semanal é insuficiente. Placa bacteriana se forma diariamente e precisa ser removida.', - weight: 4, + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: true, ); } } -// Quiz 5: Flúor (antiga Quiz 10) +// Quiz 5: Dentes em cima (Number input) class Quiz5Screen extends StatelessWidget { const Quiz5Screen({super.key, required this.currentScore, this.scopeId}); @@ -177,38 +225,20 @@ class Quiz5Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 5/20', - question: 'O flúor é seguro para crianças?', - answers: const [ - QuizAnswer( - title: 'Sim, na quantidade correta para cada idade', - description: - 'Flúor é seguro e eficaz quando usado nas quantidades recomendadas para cada faixa etária.', - weight: 2, - ), - QuizAnswer( - title: 'Não, deve ser evitado completamente', - description: - 'Flúor é essencial para prevenir cáries. O problema é o excesso, não o uso adequado.', - weight: 5, - ), - QuizAnswer( - title: 'Só necessário depois dos 12 anos', - description: - 'Flúor é importante em todas as idades, com ajuste na quantidade conforme a idade da criança.', - weight: 4, - ), - ], + title: 'Quiz 5/26', + question: 'Quantos dentes tem o seu filho/a em cima na boca?', + answers: const [], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.number, showBackButton: true, ); } } -// Quiz 6: Chupetas e Mamadeiras (antiga Quiz 11) +// Quiz 6: Dentes em baixo (Number input) class Quiz6Screen extends StatelessWidget { const Quiz6Screen({super.key, required this.currentScore, this.scopeId}); @@ -218,38 +248,20 @@ class Quiz6Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 6/20', - question: 'Até que idade é aceitável usar chupeta?', - answers: const [ - QuizAnswer( - title: 'Até 2-3 anos, com desmame gradual', - description: - 'Após 2-3 anos, chupeta pode afetar o desenvolvimento da dentição e fala. O desmame deve ser gradual.', - weight: 2, - ), - QuizAnswer( - title: 'Até os 6 anos, não tem problema', - description: - 'Uso prolongado pode causar problemas na mordida e fala, além de dificultar o alinhamento dos dentes.', - weight: 5, - ), - QuizAnswer( - title: 'Só até 1 ano', - description: - 'Um ano pode ser muito cedo para algumas crianças. O importante é começar o desmame após 2 anos.', - weight: 3, - ), - ], + title: 'Quiz 6/26', + question: 'Quantos dentes tem o seu filho/a em baixo na boca?', + answers: const [], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz7Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.number, showBackButton: true, ); } } -// Quiz 7: Bebidas e Dentição (antiga Quiz 12) +// Quiz 7: Boca (Image-based) class Quiz7Screen extends StatelessWidget { const Quiz7Screen({super.key, required this.currentScore, this.scopeId}); @@ -259,38 +271,50 @@ class Quiz7Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 7/20', - question: 'Qual bebida é mais prejudicial para os dentes das crianças?', + title: 'Quiz 7/26', + question: + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', answers: const [ QuizAnswer( - title: 'Refrigerantes e sucos industrializados', + title: 'Opção A', description: - 'Bebidas açucaradas e ácidas são as principais causas de cáries infantis, especialmente se consumidas frequentemente.', + 'Selecione se a imagem se assemelha à boca do seu filho/a', weight: 2, + imagePath: 'assets/images/mouth2_a.png', ), QuizAnswer( - title: 'Leite e água', + title: 'Opção B', description: - 'Leite e água são seguros para os dentes. O problema são bebidas açucaradas e ácidas.', - weight: 5, + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_b.png', ), QuizAnswer( - title: 'Sucos naturais sem açúcar', + title: 'Opção C', description: - 'Sucos naturais são melhores que industrializados, mas alguns são ácidos. Moderação é importante.', - weight: 3, + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: true, ); } } -// Quiz 8: Hábitos Noturnos (antiga Quiz 13) +// Quiz 8: Freio (Image-based) class Quiz8Screen extends StatelessWidget { const Quiz8Screen({super.key, required this.currentScore, this.scopeId}); @@ -300,38 +324,50 @@ class Quiz8Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 8/20', - question: 'Crianças devem escovar os dentes antes de dormir?', + title: 'Quiz 8/26', + question: + 'Qual das seguintes imagens se assemelha ao freio do seu filho/a?', answers: const [ QuizAnswer( - title: 'Sim, é fundamental antes de dormir', + title: 'Opção A', description: - 'Escovação noturna é crucial porque durante a noite a produção de saliva diminui, aumentando o risco de cáries.', + 'Selecione se a imagem se assemelha ao freio do seu filho/a', weight: 2, + imagePath: 'assets/images/frenulum_a.png', ), QuizAnswer( - title: 'Só se comeu doce', + title: 'Opção B', description: - 'Placa bacteriana se acumula durante o dia independentemente do que foi comido. Escovação noturna é sempre necessária.', - weight: 5, + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_b.png', ), QuizAnswer( - title: 'Não precisa se escovou durante o dia', + title: 'Opção C', description: - 'Mesmo com escovação diurna, a noturna é essencial devido à redução de saliva durante o sono.', - weight: 4, + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_d.png', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.image, showBackButton: true, ); } } -// Quiz 9: Traumatismos Dentários (antiga Quiz 14) +// Quiz 9: Problemas respiratórios (Yes/No) class Quiz9Screen extends StatelessWidget { const Quiz9Screen({super.key, required this.currentScore, this.scopeId}); @@ -341,38 +377,33 @@ class Quiz9Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 9/20', - question: 'O que fazer se uma criança cair e quebrar um dente?', + title: 'Quiz 9/26', + question: 'O seu filho/a tem problemas respiratórios diagnosticados?', answers: const [ QuizAnswer( - title: 'Procurar dentista imediatamente', - description: - 'Traumatismo dentário é emergência. Quanto mais rápido o atendimento, melhor o prognóstico.', + title: 'Sim', + description: 'Problemas respiratórios diagnosticados', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Esperar alguns dias para observar', - description: - 'Esperar pode comprometer o tratamento. Dentes fraturados podem infectar ou morrer se não tratados.', - weight: 5, - ), - QuizAnswer( - title: 'Dar analgésico e observar', - description: - 'Analgésico pode ajudar com dor, mas não resolve o problema dentário que precisa de tratamento profissional.', - weight: 4, + title: 'Não', + description: 'Sem problemas respiratórios diagnosticados', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz10Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 10: Selantes (antiga Quiz 15) +// Quiz 10: Respira pela boca (Yes/No) class Quiz10Screen extends StatelessWidget { const Quiz10Screen({super.key, required this.currentScore, this.scopeId}); @@ -382,38 +413,33 @@ class Quiz10Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 10/20', - question: 'Para que servem os selantes dentários?', + title: 'Quiz 10/26', + question: 'O seu filho/a respira habitualmente pela boca?', answers: const [ QuizAnswer( - title: 'Proteger contra cáries em dentes profundos', - description: - 'Selantes criam uma barreira protetora em sulcos e fissuras, locais difíceis de limpar e propensos a cáries.', + title: 'Sim', + description: 'Respira habitualmente pela boca', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Clarear os dentes', - description: - 'Selantes não têm função estética de clareamento, apenas protetora contra cáries.', - weight: 5, - ), - QuizAnswer( - title: 'Substituir a escovação', - description: - 'Selantes complementam a higiene, não substituem a escovação e o fio dental.', - weight: 4, + title: 'Não', + description: 'Não respira habitualmente pela boca', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz11Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 11: Aparelhos Ortodônticos (antiga Quiz 16) +// Quiz 11: Ressonar (Yes/No) class Quiz11Screen extends StatelessWidget { const Quiz11Screen({super.key, required this.currentScore, this.scopeId}); @@ -423,38 +449,33 @@ class Quiz11Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 11/20', - question: 'Qual a melhor idade para avaliar necessidade de aparelho?', + title: 'Quiz 11/26', + question: 'O seu filho/a ressona habitualmente durante a noite?', answers: const [ QuizAnswer( - title: 'Entre 7-9 anos para avaliação', - description: - 'Avaliação precoce permite identificar problemas e planejar o melhor momento para intervenção.', + title: 'Sim', + description: 'Ressonar habitualmente durante a noite', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Só depois dos 12 anos', - description: - 'Esperar demais pode perder a oportunidade de tratamento interceptativo que simplifica casos complexos.', - weight: 5, - ), - QuizAnswer( - title: 'Qualquer idade, não faz diferença', - description: - 'Existem momentos ideais para diferentes tipos de tratamento. Avaliação precoce é importante.', - weight: 4, + title: 'Não', + description: 'Não ressona habitualmente', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz12Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 12: Respiração Bucal (antiga Quiz 17) +// Quiz 12: Nariz tapado (Yes/No) class Quiz12Screen extends StatelessWidget { const Quiz12Screen({super.key, required this.currentScore, this.scopeId}); @@ -464,38 +485,33 @@ class Quiz12Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 12/20', - question: 'Respirar pela boca afeta os dentes?', + title: 'Quiz 12/26', + question: 'O seu filho/a sente habitualmente o nariz "tapado"?', answers: const [ QuizAnswer( - title: 'Sim, pode causar vários problemas', - description: - 'Respiração bucal pode alterar o desenvolvimento facial, causar cáries e problemas ortodônticos.', + title: 'Sim', + description: 'Sente habitualmente o nariz tapado', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Não, é apenas uma questão de hábito', - description: - 'Respiração bucal tem consequências reais na saúde bucal e desenvolvimento facial da criança.', - weight: 5, - ), - QuizAnswer( - title: 'Só afeta adultos, não crianças', - description: - 'Em crianças, os efeitos são mais sérios pois afetam o desenvolvimento dos ossos faciais.', - weight: 4, + title: 'Não', + description: 'Não sente habitualmente o nariz tapado', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz13Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 13: Gengivas (antiga Quiz 18) +// Quiz 13: Interrupções respiração (Yes/No) class Quiz13Screen extends StatelessWidget { const Quiz13Screen({super.key, required this.currentScore, this.scopeId}); @@ -505,38 +521,35 @@ class Quiz13Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 13/20', - question: 'O que causa gengivas inflamadas em crianças?', + title: 'Quiz 13/26', + question: + 'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?', answers: const [ QuizAnswer( - title: 'Higiene inadequada e acúmulo de placa', + title: 'Sim', description: - 'Placa bacteriana não removida properly causa inflamação gengival, a forma mais comum de gengivite.', + 'Tem habitualmente interrupções da respiração durante o sono', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'É normal na infância, não precisa tratar', - description: - 'Gengivite não é normal e precisa tratamento. Se não tratada, pode evoluir para periodontite.', - weight: 5, - ), - QuizAnswer( - title: 'Apenas mudanças hormonais', - description: - 'Hormônios podem influenciar, mas a causa principal é acúmulo de placa por higiene inadequada.', - weight: 4, + title: 'Não', + description: 'Não tem interrupções da respiração durante o sono', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz14Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 14: Lanche Escolar (antiga Quiz 19) +// Quiz 14: Range os dentes (Yes/No) class Quiz14Screen extends StatelessWidget { const Quiz14Screen({super.key, required this.currentScore, this.scopeId}); @@ -546,38 +559,33 @@ class Quiz14Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 14/20', - question: 'Qual lanche escolar é melhor para os dentes?', + title: 'Quiz 14/26', + question: 'O seu filho/a range os dentes com frequência?', answers: const [ QuizAnswer( - title: 'Frutas, queijo e água', - description: - 'Lanches naturais e sem açúcar são ideais. Queijo até ajuda neutralizar ácidos e fortalecer dentes.', + title: 'Sim', + description: 'Range os dentes com frequência', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Bolachas recheadas e suco de caixa', - description: - 'Lanches industrializados e açucarados são os principais vilões da saúde bucal escolar.', - weight: 5, - ), - QuizAnswer( - title: 'Salgadinhos de pacote', - description: - 'Salgadinhos são amiláceos e se transformam em açúcar, além de ficarem presos nos dentes.', - weight: 4, + title: 'Não', + description: 'Não range os dentes com frequência', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz15Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 15: Medo do Dentista (antiga Quiz 20) +// Quiz 15: Alergias sazonais (Yes/No) class Quiz15Screen extends StatelessWidget { const Quiz15Screen({super.key, required this.currentScore, this.scopeId}); @@ -587,38 +595,33 @@ class Quiz15Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 15/20', - question: 'Como lidar com o medo do dentista em crianças?', + title: 'Quiz 15/26', + question: 'O seu filho/a habitualmente tem alergias sazonais?', answers: const [ QuizAnswer( - title: 'Conversar positivamente e visitar regularmente', - description: - 'Linguagem positiva e visitas frequentes sem necessidade de tratamento ajudam a criar confiança.', + title: 'Sim', + description: 'Habitualmente tem alergias sazonais', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Evitar falar sobre dentista', - description: - 'Não falar sobre o assunto pode aumentar o medo. É importante preparar a criança positivamente.', - weight: 5, - ), - QuizAnswer( - title: 'Levar só quando houver problema', - description: - 'Visitas só em caso de problema associam dentista a dor. Visitas regulares preventivas são melhores.', - weight: 4, + title: 'Não', + description: 'Não tem alergias sazonais', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz16Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 16: Tempo ideal para escovar (antiga Quiz 1) +// Quiz 16: Saliva seca (Yes/No) class Quiz16Screen extends StatelessWidget { const Quiz16Screen({super.key, required this.currentScore, this.scopeId}); @@ -628,38 +631,33 @@ class Quiz16Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 16/20', - question: 'Qual é o tempo ideal para escovar os dentes?', + title: 'Quiz 16/26', + question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?', answers: const [ QuizAnswer( - title: 'Cerca de 2 minutos', - description: - 'O recomendado é escovar por aproximadamente 2 minutos, cobrindo todas as superfícies dos dentes e a linha da gengiva sem pressa.', + title: 'Sim', + description: 'Acorda com saliva seca na cara ou na almofada', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Só 30 segundos, se fizer rápido', - description: - 'Muito pouco tempo costuma deixar placa bacteriana para trás, principalmente nos dentes de trás e perto da gengiva.', - weight: 5, - ), - QuizAnswer( - title: '5 minutos com força para "limpar bem"', - description: - 'Tempo demais e força excessiva podem irritar a gengiva e desgastar o esmalte. Prefira movimentos suaves e tempo adequado.', - weight: 3, + title: 'Não', + description: 'Não acorda com saliva seca', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz17Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 17: Troca da escova (antiga Quiz 2) +// Quiz 17: Otites (Yes/No) class Quiz17Screen extends StatelessWidget { const Quiz17Screen({super.key, required this.currentScore, this.scopeId}); @@ -669,38 +667,33 @@ class Quiz17Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 17/20', - question: 'Quando devo trocar a escova de dentes?', + title: 'Quiz 17/26', + question: 'O seu filho/a teve ou costuma ter com frequência otites?', answers: const [ QuizAnswer( - title: 'A cada 3 meses (ou antes se estragar)', - description: - 'O ideal é trocar a cada ~3 meses. Se as cerdas abrirem antes, troque antes. Cerdas abertas limpam pior.', + title: 'Sim', + description: 'Teve ou costuma ter com frequência otites', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Só quando a escova "quebrar"', - description: - 'Esperar demais reduz a eficiência da escovação e pode acumular microrganismos na escova.', - weight: 5, - ), - QuizAnswer( - title: 'Todo mês, obrigatoriamente', - description: - 'Não é regra fixa. Um mês pode ser cedo demais se a escova estiver em bom estado. O principal é o estado das cerdas.', - weight: 3, + title: 'Não', + description: 'Não teve ou não costuma ter otites', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz18Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 18: Quantidade de pasta (antiga Quiz 3) +// Quiz 18: Amigdalites (Yes/No) class Quiz18Screen extends StatelessWidget { const Quiz18Screen({super.key, required this.currentScore, this.scopeId}); @@ -710,38 +703,33 @@ class Quiz18Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 18/20', - question: 'Qual a quantidade ideal de pasta de dente para crianças?', + title: 'Quiz 18/26', + question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?', answers: const [ QuizAnswer( - title: 'Um grão de arroz (pequenos) / ervilha (maiores)', - description: - 'Para crianças pequenas, um "grão de arroz" já basta. Conforme cresce, pode ser do tamanho de uma ervilha. Isso ajuda a evitar excesso de flúor ingerido.', + title: 'Sim', + description: 'Teve ou costuma ter com frequência amigdalites', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Cobrir toda a escova com pasta', - description: - 'Muito produto não significa melhor limpeza. Em crianças, aumenta o risco de engolir pasta em excesso.', - weight: 5, - ), - QuizAnswer( - title: 'Nenhuma pasta, só água', - description: - 'A pasta com flúor (na quantidade correta) ajuda a prevenir cáries. Em geral, água sozinha não oferece a mesma proteção.', - weight: 3, + title: 'Não', + description: 'Não teve ou não costuma ter amigdalites', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz19Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 19: Fio dental (antiga Quiz 4) +// Quiz 19: Bronquiolites (Yes/No) class Quiz19Screen extends StatelessWidget { const Quiz19Screen({super.key, required this.currentScore, this.scopeId}); @@ -751,38 +739,34 @@ class Quiz19Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 19/20', - question: 'Qual é o melhor horário para usar fio dental?', + title: 'Quiz 19/26', + question: + 'O seu filho/a teve ou costuma ter com frequência bronquiolites?', answers: const [ QuizAnswer( - title: 'Uma vez ao dia, com calma (geralmente à noite)', - description: - 'O importante é a frequência diária. À noite costuma ser mais fácil, pois remove restos e placa antes de dormir.', + title: 'Sim', + description: 'Teve ou costuma ter com frequência bronquiolites', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Só quando algo fica preso', - description: - 'O fio dental não serve apenas para tirar restos visíveis; ele remove placa bacteriana entre os dentes onde a escova não alcança.', - weight: 5, - ), - QuizAnswer( - title: 'Depois de toda refeição (obrigatório)', - description: - 'Pode ser útil em alguns casos, mas não é obrigatório para todos. O essencial é fazer bem feito ao menos 1x ao dia.', - weight: 3, + title: 'Não', + description: 'Não teve ou não costuma ter bronquiolites', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( builder: (_) => Quiz20Screen(currentScore: nextScore, scopeId: scopeId), ), + answerType: QuizAnswerType.yesNo, showBackButton: true, ); } } -// Quiz 20: Prevenção de cáries (antiga Quiz 5) +// Quiz 20: Dificuldades a mastigar (Yes/No) class Quiz20Screen extends StatelessWidget { const Quiz20Screen({super.key, required this.currentScore, this.scopeId}); @@ -792,31 +776,238 @@ class Quiz20Screen extends StatelessWidget { @override Widget build(BuildContext context) { return QuizQuestionScreen( - title: 'Quiz 20/20', - question: 'O que ajuda mais a prevenir cáries no dia a dia?', + title: 'Quiz 20/26', + question: 'O seu filho/a apresenta dificuldades a mastigar?', answers: const [ QuizAnswer( - title: 'Escovar + flúor + reduzir açúcar frequente', - description: - 'A prevenção é um conjunto: boa higiene com flúor e menos "beliscos" açucarados ao longo do dia.', + title: 'Sim', + description: 'Apresenta dificuldades a mastigar', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Só enxaguante bucal', - description: - 'Enxaguante pode ajudar em alguns casos, mas não substitui escovação e fio dental.', - weight: 3, - ), - QuizAnswer( - title: 'Evitar completamente dentista', - description: - 'Consultas regulares são importantes para prevenção e orientação. O dentista também identifica problemas bem no começo.', - weight: 5, + title: 'Não', + description: 'Não apresenta dificuldades a mastigar', + weight: 1, + value: 'nao', ), ], currentScore: currentScore, nextRoute: (context, nextScore) => MaterialPageRoute( - builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 100, scopeId: scopeId), + builder: (_) => Quiz21Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 21: Lento a comer (Yes/No) +class Quiz21Screen extends StatelessWidget { + const Quiz21Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 21/26', + question: 'O seu filho/a habitualmente é lento a comer?', + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Habitualmente é lento a comer', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não é lento a comer', + weight: 1, + value: 'nao', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => Quiz22Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 22: Prefere alimentos moles (Yes/No) +class Quiz22Screen extends StatelessWidget { + const Quiz22Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 22/26', + question: 'O seu filho/a habitualmente prefere comer alimentos moles?', + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Habitualmente prefere comer alimentos moles', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não prefere alimentos moles', + weight: 1, + value: 'nao', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => Quiz23Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 23: Alimentado por biberão (Yes/No) +class Quiz23Screen extends StatelessWidget { + const Quiz23Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 23/26', + question: 'Em bebé apenas foi alimentado por biberão?', + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Em bebé apenas foi alimentado por biberão', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não foi apenas alimentado por biberão', + weight: 1, + value: 'nao', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => Quiz24Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 24: Chupeta (Yes/No) +class Quiz24Screen extends StatelessWidget { + const Quiz24Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 24/26', + question: 'O seu filho/a usa ou usou chupeta com frequência?', + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Usa ou usou chupeta com frequência', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não usa ou não usou chupeta com frequência', + weight: 1, + value: 'nao', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => Quiz25Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 25: Chucha o dedo (Yes/No) +class Quiz25Screen extends StatelessWidget { + const Quiz25Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 25/26', + question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?', + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Chucha ou já chuchou o dedo com frequência', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não chucha ou não chuchou o dedo com frequência', + weight: 1, + value: 'nao', + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => Quiz26Screen(currentScore: nextScore, scopeId: scopeId), + ), + answerType: QuizAnswerType.yesNo, + showBackButton: true, + ); + } +} + +// Quiz 26: Final +class Quiz26Screen extends StatelessWidget { + const Quiz26Screen({super.key, required this.currentScore, this.scopeId}); + + final int currentScore; + final String? scopeId; + + @override + Widget build(BuildContext context) { + return QuizQuestionScreen( + title: 'Quiz 26/26', + question: 'Obrigado por completar o questionário!', + answers: const [ + QuizAnswer( + title: 'Concluir', + description: 'Clique para ver os resultados', + weight: 0, + ), + ], + currentScore: currentScore, + nextRoute: (context, nextScore) => MaterialPageRoute( + builder: (_) => QuizResultScreen( + finalScore: nextScore, + maxScore: 130, + scopeId: scopeId, + ), ), isFinal: true, showBackButton: true, diff --git a/lib/quiz/quiz_question_screen.dart b/lib/quiz/quiz_question_screen.dart index 9fe8a33..64e1a1a 100644 --- a/lib/quiz/quiz_question_screen.dart +++ b/lib/quiz/quiz_question_screen.dart @@ -3,14 +3,25 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -typedef QuizNextBuilder = Route Function(BuildContext context, int nextScore); +typedef QuizNextBuilder = + Route Function(BuildContext context, int nextScore); + +enum QuizAnswerType { text, image, number, yesNo } class QuizAnswer { - const QuizAnswer({required this.title, required this.description, required this.weight}); + const QuizAnswer({ + required this.title, + required this.description, + required this.weight, + this.imagePath, + this.value, + }); final String title; final String description; final int weight; + final String? imagePath; + final String? value; } class QuizQuestionScreen extends StatefulWidget { @@ -24,6 +35,7 @@ class QuizQuestionScreen extends StatefulWidget { this.onFinished, this.isFinal = false, this.showBackButton = false, + this.answerType = QuizAnswerType.text, }); final String title; @@ -34,6 +46,7 @@ class QuizQuestionScreen extends StatefulWidget { final VoidCallback? onFinished; final bool isFinal; final bool showBackButton; + final QuizAnswerType answerType; @override State createState() => _QuizQuestionScreenState(); @@ -41,11 +54,30 @@ class QuizQuestionScreen extends StatefulWidget { class _QuizQuestionScreenState extends State { int? _selected; + TextEditingController? _numberController; + int? _numberValue; + + @override + void initState() { + super.initState(); + if (widget.answerType == QuizAnswerType.number) { + _numberController = TextEditingController(); + } + } + + @override + void dispose() { + _numberController?.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final size = MediaQuery.sizeOf(context); - final bool canProceed = _selected != null; + bool canProceed = _selected != null; + if (widget.answerType == QuizAnswerType.number) { + canProceed = _numberValue != null && _numberValue! >= 0; + } return Scaffold( body: Stack( @@ -57,10 +89,7 @@ class _QuizQuestionScreenState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFE6F1), - Color(0xFFFFC9DF), - ], + colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], ), ), ), @@ -119,7 +148,11 @@ class _QuizQuestionScreenState extends State { ), const SizedBox(height: 8), Text( - 'Escolha apenas uma opção', + widget.answerType == QuizAnswerType.number + ? 'Insira o número' + : widget.answerType == QuizAnswerType.yesNo + ? 'Escolha uma opção' + : 'Escolha apenas uma opção', textAlign: TextAlign.center, style: TextStyle( color: Colors.black.withValues(alpha: 0.55), @@ -132,18 +165,21 @@ class _QuizQuestionScreenState extends State { Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: ListView.separated( - padding: const EdgeInsets.only(bottom: 12), - itemCount: widget.answers.length, - separatorBuilder: (context, index) => const SizedBox(height: 12), - itemBuilder: (context, i) { - return _QuizAnswerTile( - answer: widget.answers[i], - selected: _selected == i, - onTap: () => setState(() => _selected = i), - ); - }, - ), + child: widget.answerType == QuizAnswerType.number + ? _buildNumberInput() + : ListView.separated( + padding: const EdgeInsets.only(bottom: 12), + itemCount: widget.answers.length, + separatorBuilder: (context, index) => + const SizedBox(height: 12), + itemBuilder: (context, i) { + return _QuizAnswerTile( + answer: widget.answers[i], + selected: _selected == i, + onTap: () => setState(() => _selected = i), + ); + }, + ), ), ), Padding( @@ -154,41 +190,77 @@ class _QuizQuestionScreenState extends State { width: size.width * 0.62, height: 46, child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: const Color(0xFF2F9E94), - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w900), - ).copyWith( - animationDuration: const Duration(milliseconds: 180), - splashFactory: InkSparkle.splashFactory, - overlayColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.pressed)) { - return Colors.white.withValues(alpha: 0.14); - } - if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { - return Colors.white.withValues(alpha: 0.08); - } - return null; - }, - ), - ), + style: + FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontWeight: FontWeight.w900, + ), + ).copyWith( + animationDuration: const Duration( + milliseconds: 180, + ), + splashFactory: InkSparkle.splashFactory, + overlayColor: + WidgetStateProperty.resolveWith( + (states) { + if (states.contains( + WidgetState.pressed, + )) { + return Colors.white.withValues( + alpha: 0.14, + ); + } + if (states.contains( + WidgetState.hovered, + ) || + states.contains( + WidgetState.focused, + )) { + return Colors.white.withValues( + alpha: 0.08, + ); + } + return null; + }, + ), + ), onPressed: !canProceed ? null : () { - final picked = widget.answers[_selected!]; - final nextScore = widget.currentScore + picked.weight; + int nextScore = widget.currentScore; + if (widget.answerType == + QuizAnswerType.number) { + nextScore = + widget.currentScore + + (_numberValue ?? 0); + } else { + final picked = + widget.answers[_selected!]; + nextScore = + widget.currentScore + picked.weight; + } if (widget.isFinal) { - widget.onFinished?.call(); - Navigator.of(context).popUntil((r) => r.isFirst); + final finishedRoute = widget.nextRoute( + context, + nextScore, + ); + Navigator.of( + context, + ).pushReplacement(finishedRoute); return; } - Navigator.of(context).push(widget.nextRoute(context, nextScore)); + Navigator.of(context).push( + widget.nextRoute(context, nextScore), + ); }, - child: Text(widget.isFinal ? 'Concluir' : 'Avançar'), + child: Text( + widget.isFinal ? 'Concluir' : 'Avançar', + ), ), ), if (widget.showBackButton) ...[ @@ -197,27 +269,45 @@ class _QuizQuestionScreenState extends State { width: size.width * 0.62, height: 42, child: FilledButton( - style: FilledButton.styleFrom( - backgroundColor: const Color(0xFF2F9E94), - foregroundColor: Colors.white, - shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w900), - ).copyWith( - animationDuration: const Duration(milliseconds: 180), - splashFactory: InkSparkle.splashFactory, - overlayColor: WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.pressed)) { - return Colors.white.withValues(alpha: 0.14); - } - if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused)) { - return Colors.white.withValues(alpha: 0.08); - } - return null; - }, - ), - ), - onPressed: () => Navigator.of(context).maybePop(), + style: + FilledButton.styleFrom( + backgroundColor: const Color(0xFF2F9E94), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontWeight: FontWeight.w900, + ), + ).copyWith( + animationDuration: const Duration( + milliseconds: 180, + ), + splashFactory: InkSparkle.splashFactory, + overlayColor: + WidgetStateProperty.resolveWith< + Color? + >((states) { + if (states.contains( + WidgetState.pressed, + )) { + return Colors.white.withValues( + alpha: 0.14, + ); + } + if (states.contains( + WidgetState.hovered, + ) || + states.contains( + WidgetState.focused, + )) { + return Colors.white.withValues( + alpha: 0.08, + ); + } + return null; + }), + ), + onPressed: () => + Navigator.of(context).maybePop(), child: const Text('Voltar'), ), ), @@ -234,10 +324,67 @@ class _QuizQuestionScreenState extends State { ), ); } + + Widget _buildNumberInput() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 150, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.70), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.black.withValues(alpha: 0.12), + width: 1.0, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.06), + blurRadius: 18, + offset: const Offset(0, 10), + ), + ], + ), + child: TextField( + controller: _numberController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: Color(0xFF2F9E94), + ), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: '0', + hintStyle: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w900, + color: Colors.grey, + ), + contentPadding: EdgeInsets.symmetric(vertical: 20), + ), + onChanged: (value) { + setState(() { + _numberValue = int.tryParse(value); + }); + }, + ), + ), + ], + ), + ); + } } class _QuizAnswerTile extends StatelessWidget { - const _QuizAnswerTile({required this.answer, required this.selected, required this.onTap}); + const _QuizAnswerTile({ + required this.answer, + required this.selected, + required this.onTap, + }); final QuizAnswer answer; final bool selected; @@ -245,8 +392,12 @@ class _QuizAnswerTile extends StatelessWidget { @override Widget build(BuildContext context) { - final borderColor = selected ? const Color(0xFF2F9E94) : Colors.black.withValues(alpha: 0.12); - final bg = selected ? Colors.white.withValues(alpha: 0.88) : Colors.white.withValues(alpha: 0.70); + final borderColor = selected + ? const Color(0xFF2F9E94) + : Colors.black.withValues(alpha: 0.12); + final bg = selected + ? Colors.white.withValues(alpha: 0.88) + : Colors.white.withValues(alpha: 0.70); return AnimatedContainer( duration: const Duration(milliseconds: 220), @@ -310,7 +461,9 @@ class _QuizAnswerTile extends StatelessWidget { ), ), ), - crossFadeState: selected ? CrossFadeState.showSecond : CrossFadeState.showFirst, + crossFadeState: selected + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, duration: const Duration(milliseconds: 220), firstCurve: Curves.easeIn, secondCurve: Curves.easeOut, diff --git a/lib/quiz/quiz_random.dart b/lib/quiz/quiz_random.dart index d36030b..d3c0d07 100644 --- a/lib/quiz/quiz_random.dart +++ b/lib/quiz/quiz_random.dart @@ -19,331 +19,580 @@ class _QuizRandomScreenState extends State { final List _allQuestions = [ QuizQuestion( id: 1, - title: 'Quiz 1/15', - question: 'Qual é o tempo ideal para escovar os dentes?', + title: 'Quiz 1/26', + question: + 'Qual das seguintes imagens se assemelha à face do seu filho/a?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'Cerca de 2 minutos', - description: 'O recomendado é escovar por aproximadamente 2 minutos, cobrindo todas as superfícies dos dentes e a linha da gengiva sem pressa.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha à face do seu filho/a', weight: 2, + imagePath: 'assets/images/face_a.png', ), QuizAnswer( - title: 'Só 30 segundos, se fizer rápido', - description: 'Muito pouco tempo costuma deixar placa bacteriana para trás, principalmente nos dentes de trás e perto da gengiva.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_b.png', ), QuizAnswer( - title: '5 minutos com força para "limpar bem"', - description: 'Tempo demais e força excessiva podem irritar a gengiva e desgastar o esmalte. Prefira movimentos suaves e tempo adequado.', - weight: 3, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à face do seu filho/a', + weight: 2, + imagePath: 'assets/images/face_d.png', ), ], ), QuizQuestion( id: 2, - title: 'Quiz 2/15', - question: 'Quando devo trocar a escova de dentes?', + title: 'Quiz 2/26', + question: + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'A cada 3 meses (ou antes se estragar)', - description: 'O ideal é trocar a cada ~3 meses. Se as cerdas abrirem antes, troque antes. Cerdas abertas limpam pior.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', weight: 2, + imagePath: 'assets/images/mouth_a.png', ), QuizAnswer( - title: 'Só quando a escova "quebrar"', - description: 'Esperar demais reduz a eficiência da escovação e pode acumular microrganismos na escova.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_b.png', ), QuizAnswer( - title: 'Todo mês, obrigatoriamente', - description: 'Não é regra fixa. Um mês pode ser cedo demais se a escova estiver em bom estado. O principal é o estado das cerdas.', - weight: 3, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth_d.png', ), ], ), QuizQuestion( id: 3, - title: 'Quiz 3/15', - question: 'Qual a quantidade ideal de pasta de dente para crianças?', + title: 'Quiz 3/26', + question: + 'Qual das seguintes imagens se assemelha às olheiras do seu filho/a?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'Um grão de arroz (pequenos) / ervilha (maiores)', - description: 'Para crianças pequenas, um "grão de arroz" já basta. Conforme cresce, pode ser do tamanho de uma ervilha. Isso ajuda a evitar excesso de flúor ingerido.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', weight: 2, + imagePath: 'assets/images/dark_circles_a.png', ), QuizAnswer( - title: 'Cobrir toda a escova com pasta', - description: 'Muito produto não significa melhor limpeza. Em crianças, aumenta o risco de engolir pasta em excesso.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_b.png', ), QuizAnswer( - title: 'Nenhuma pasta, só água', - description: 'A pasta com flúor (na quantidade correta) ajuda a prevenir cáries. Em geral, água sozinha não oferece a mesma proteção.', - weight: 3, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha às olheiras do seu filho/a', + weight: 2, + imagePath: 'assets/images/dark_circles_d.png', ), ], ), QuizQuestion( id: 4, - title: 'Quiz 4/15', - question: 'Qual é o melhor horário para usar fio dental?', + title: 'Quiz 4/26', + question: + 'Qual das seguintes imagens se assemelha ao queixo do seu filho/a com a boca fechada?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'Uma vez ao dia, com calma (geralmente à noite)', - description: 'O importante é a frequência diária. À noite costuma ser mais fácil, pois remove restos e placa antes de dormir.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', weight: 2, + imagePath: 'assets/images/chin_a.png', ), QuizAnswer( - title: 'Só quando algo fica preso', - description: 'O fio dental não serve apenas para tirar restos visíveis; ele remove placa bacteriana entre os dentes onde a escova não alcança.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_b.png', ), QuizAnswer( - title: 'Depois de toda refeição (obrigatório)', - description: 'Pode ser útil em alguns casos, mas não é obrigatório para todos. O essencial é fazer bem feito ao menos 1x ao dia.', - weight: 3, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha ao queixo do seu filho/a', + weight: 2, + imagePath: 'assets/images/chin_d.png', ), ], ), QuizQuestion( id: 5, - title: 'Quiz 5/15', - question: 'O que ajuda mais a prevenir cáries no dia a dia?', - answers: const [ - QuizAnswer( - title: 'Escovar + flúor + reduzir açúcar frequente', - description: 'A prevenção é um conjunto: boa higiene com flúor e menos "beliscos" açucarados ao longo do dia.', - weight: 2, - ), - QuizAnswer( - title: 'Só enxaguante bucal', - description: 'Enxaguante pode ajudar em alguns casos, mas não substitui escovação e fio dental.', - weight: 3, - ), - QuizAnswer( - title: 'Evitar completamente dentista', - description: 'Consultas regulares são importantes para prevenção e orientação. O dentista também identifica problemas bem no começo.', - weight: 5, - ), - ], + title: 'Quiz 5/26', + question: 'Quantos dentes tem o seu filho/a em cima na boca?', + answerType: QuizAnswerType.number, + answers: const [], ), QuizQuestion( id: 6, - title: 'Quiz 6/15', - question: 'Qual tipo de escova é mais recomendada para crianças?', - answers: const [ - QuizAnswer( - title: 'Escova macia com cabeça pequena', - description: 'Escovas macias protegem a gengiva sensível das crianças e a cabeça pequena alcança melhor todos os dentes.', - weight: 2, - ), - QuizAnswer( - title: 'Escova dura para limpar melhor', - description: 'Escovas duras podem machucar a gengiva e desgastar o esmalte dos dentes das crianças.', - weight: 5, - ), - QuizAnswer( - title: 'Escova elétrica sempre é melhor', - description: 'Escova elétrica pode ajudar, mas não é essencial. O mais importante é a técnica e frequência.', - weight: 3, - ), - ], + title: 'Quiz 6/26', + question: 'Quantos dentes tem o seu filho/a em baixo na boca?', + answerType: QuizAnswerType.number, + answers: const [], ), QuizQuestion( id: 7, - title: 'Quiz 7/15', - question: 'Qual alimento é mais prejudicial para os dentes?', + title: 'Quiz 7/26', + question: + 'Qual das seguintes imagens se assemelha à boca do seu filho/a?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'Balas e chicletes pegajosos', - description: 'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', weight: 2, + imagePath: 'assets/images/mouth2_a.png', ), QuizAnswer( - title: 'Maçã e cenoura', - description: 'Frutas e vegetais crus ajudam a limpar os dentes naturalmente e são saudáveis.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_b.png', ), QuizAnswer( - title: 'Água e leite', - description: 'Água ajuda a limpar e leite tem cálcio. São opções saudáveis para os dentes.', - weight: 3, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha à boca do seu filho/a', + weight: 2, + imagePath: 'assets/images/mouth2_d.png', ), ], ), QuizQuestion( id: 8, - title: 'Quiz 8/15', - question: 'Quando deve ser a primeira visita ao dentista?', + title: 'Quiz 8/26', + question: + 'Qual das seguintes imagens se assemelha ao freio do seu filho/a?', + answerType: QuizAnswerType.image, answers: const [ QuizAnswer( - title: 'Por volta dos 1 ano de idade', - description: 'A primeira visita deve ser assim que o primeiro dentinho nascer ou até o primeiro aniversário.', + title: 'Opção A', + description: + 'Selecione se a imagem se assemelha ao freio do seu filho/a', weight: 2, + imagePath: 'assets/images/frenulum_a.png', ), QuizAnswer( - title: 'Só quando tiver todos os dentes', - description: 'Esperar demais pode permitir que problemas comecem sem detecção precoce.', - weight: 5, + title: 'Opção B', + description: + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_b.png', ), QuizAnswer( - title: 'Apenas se sentir dor', - description: 'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.', - weight: 5, + title: 'Opção C', + description: + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_c.png', + ), + QuizAnswer( + title: 'Opção D', + description: + 'Selecione se a imagem se assemelha ao freio do seu filho/a', + weight: 2, + imagePath: 'assets/images/frenulum_d.png', ), ], ), QuizQuestion( id: 9, - title: 'Quiz 9/15', - question: 'Até que idade é aceitável usar chupeta?', + title: 'Quiz 9/26', + question: 'O seu filho/a tem problemas respiratórios diagnosticados?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Até 2-3 anos no máximo', - description: 'Após 2-3 anos, chupeta pode causar problemas na dentição e no desenvolvimento da fala.', + title: 'Sim', + description: 'Problemas respiratórios diagnosticados', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Até 6-7 anos', - description: 'Essa idade já é muito tarde e pode causar problemas sérios na arcada dentária.', - weight: 5, - ), - QuizAnswer( - title: 'Não tem problema usar sempre', - description: 'Uso prolongado pode causar má oclusão, problemas na fala e alterações faciais.', - weight: 5, + title: 'Não', + description: 'Sem problemas respiratórios diagnosticados', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 10, - title: 'Quiz 10/15', - question: 'O flúor na água de abastecimento ajuda?', + title: 'Quiz 10/26', + question: 'O seu filho/a respira habitualmente pela boca?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Sim, reduz cáries em até 60%', - description: 'Flúor na água é uma das medidas de saúde pública mais eficazes na prevenção de cáries.', + title: 'Sim', + description: 'Respira habitualmente pela boca', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Não faz diferença nenhuma', - description: 'Estudos comprovam que flúor na água reduz significativamente a incidência de cáries.', - weight: 5, - ), - QuizAnswer( - title: 'É perigoso e causa problemas', - description: 'Nas concentrações corretas, flúor é seguro. O problema é o excesso, não o uso adequado.', - weight: 4, + title: 'Não', + description: 'Não respira habitualmente pela boca', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 11, - title: 'Quiz 11/15', - question: 'Por que a escovação noturna é tão importante?', + title: 'Quiz 11/26', + question: 'O seu filho/a ressona habitualmente durante a noite?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Menos saliva durante o sono', - description: 'Durante a noite produzimos menos saliva, que protege os dentes. Escovação remove placa antes desse período vulnerável.', + title: 'Sim', + description: 'Ressonar habitualmente durante a noite', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'É igual aos outros horários', - description: 'A noite é especial porque a produção de saliva diminui, aumentando o risco de cáries.', - weight: 4, - ), - QuizAnswer( - title: 'Só por tradição', - description: 'Tem fundamento científico. A noite é o período mais crítico para formação de cáries.', - weight: 5, + title: 'Não', + description: 'Não ressona habitualmente', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 12, - title: 'Quiz 12/15', - question: 'Qual bebida é mais ácida para os dentes?', + title: 'Quiz 12/26', + question: 'O seu filho/a sente habitualmente o nariz "tapado"?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Refrigerantes e sucos industrializados', - description: 'Refrigerantes e sucos artificiais têm pH muito baixo, corroem o esmalte e causam erosão dental.', + title: 'Sim', + description: 'Sente habitualmente o nariz tapado', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Água e leite', - description: 'Água tem pH neutro e leite é levemente ácido mas protege os dentes com cálcio.', - weight: 5, - ), - QuizAnswer( - title: 'Chá sem açúcar', - description: 'Chá pode manchar mas é muito menos ácido que refrigerantes e sucos artificiais.', - weight: 3, + title: 'Não', + description: 'Não sente habitualmente o nariz tapado', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 13, - title: 'Quiz 13/15', - question: 'É importante cuidar dos dentes de leite?', + title: 'Quiz 13/26', + question: + 'Durante o sono, o seu filho/a tem habitualmente interrupções da respiração?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Sim, são fundamentais para o desenvolvimento', - description: 'Dentes de leite mantêm espaço para os permanentes, auxiliam na fala e mastigação.', + title: 'Sim', + description: + 'Tem habitualmente interrupções da respiração durante o sono', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Não, vão cair de qualquer jeito', - description: 'Dentes de leite doentes podem afetar os permanentes e causar problemas no desenvolvimento.', - weight: 5, - ), - QuizAnswer( - title: 'Só se doerem', - description: 'Mesmo sem dor, problemas nos dentes de leite podem ter consequências sérias futuras.', - weight: 4, + title: 'Não', + description: 'Não tem interrupções da respiração durante o sono', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 14, - title: 'Quiz 14/15', - question: 'Qual é a técnica correta de escovação?', + title: 'Quiz 14/26', + question: 'O seu filho/a range os dentes com frequência?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Movimentos circulares suaves', - description: 'Movimentos circulares ou vibratórios suaves limpam sem machucar a gengiva e removem a placa eficientemente.', + title: 'Sim', + description: 'Range os dentes com frequência', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Força de um lado para o outro', - description: 'Movimentos horizontais fortes podem machucar a gengiva e causar recessão gengival.', - weight: 5, - ), - QuizAnswer( - title: 'Só na frente dos dentes', - description: 'Precisa escovar todas as faces: frente, atrás e superfície de mastigação.', - weight: 4, + title: 'Não', + description: 'Não range os dentes com frequência', + weight: 1, + value: 'nao', ), ], ), QuizQuestion( id: 15, - title: 'Quiz 15/15', - question: 'Para que servem os selantes dentários?', + title: 'Quiz 15/26', + question: 'O seu filho/a habitualmente tem alergias sazonais?', + answerType: QuizAnswerType.yesNo, answers: const [ QuizAnswer( - title: 'Proteger sulcos dos dentes contra cáries', - description: 'Selantes são uma resina que preenche sulcos e fissuras dos dentes, protegendo contra cáries.', + title: 'Sim', + description: 'Habitualmente tem alergias sazonais', weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Clarear os dentes', - description: 'Selantes não têm função estética de clareamento, apenas protetiva contra cáries.', - weight: 5, + title: 'Não', + description: 'Não tem alergias sazonais', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 16, + title: 'Quiz 16/26', + question: 'O seu filho/a acorda com saliva seca na cara ou na almofada?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Acorda com saliva seca na cara ou na almofada', + weight: 2, + value: 'sim', ), QuizAnswer( - title: 'Substituir a escovação', - description: 'Selantes complementam a higiene, não substituem a escovação e o fio dental.', - weight: 4, + title: 'Não', + description: 'Não acorda com saliva seca', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 17, + title: 'Quiz 17/26', + question: 'O seu filho/a teve ou costuma ter com frequência otites?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Teve ou costuma ter com frequência otites', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não teve ou não costuma ter otites', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 18, + title: 'Quiz 18/26', + question: 'O seu filho/a teve ou costuma ter com frequência amigdalites?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Teve ou costuma ter com frequência amigdalites', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não teve ou não costuma ter amigdalites', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 19, + title: 'Quiz 19/26', + question: + 'O seu filho/a teve ou costuma ter com frequência bronquiolites?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Teve ou costuma ter com frequência bronquiolites', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não teve ou não costuma ter bronquiolites', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 20, + title: 'Quiz 20/26', + question: 'O seu filho/a apresenta dificuldades a mastigar?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Apresenta dificuldades a mastigar', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não apresenta dificuldades a mastigar', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 21, + title: 'Quiz 21/26', + question: 'O seu filho/a habitualmente é lento a comer?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Habitualmente é lento a comer', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não é lento a comer', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 22, + title: 'Quiz 22/26', + question: 'O seu filho/a habitualmente prefere comer alimentos moles?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Habitualmente prefere comer alimentos moles', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não prefere alimentos moles', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 23, + title: 'Quiz 23/26', + question: 'Em bebé apenas foi alimentado por biberão?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Em bebé apenas foi alimentado por biberão', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não foi apenas alimentado por biberão', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 24, + title: 'Quiz 24/26', + question: 'O seu filho/a usa ou usou chupeta com frequência?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Usa ou usou chupeta com frequência', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não usa ou não usou chupeta com frequência', + weight: 1, + value: 'nao', + ), + ], + ), + QuizQuestion( + id: 25, + title: 'Quiz 25/26', + question: 'O seu filho/a chucha ou já chuchou o dedo com frequência?', + answerType: QuizAnswerType.yesNo, + answers: const [ + QuizAnswer( + title: 'Sim', + description: 'Chucha ou já chuchou o dedo com frequência', + weight: 2, + value: 'sim', + ), + QuizAnswer( + title: 'Não', + description: 'Não chucha ou não chuchou o dedo com frequência', + weight: 1, + value: 'nao', ), ], ), @@ -388,7 +637,8 @@ class _QuizRandomScreenState extends State { } final currentQuestion = _shuffledQuestions[_currentQuestionIndex]; - final isLastQuestion = _currentQuestionIndex == _shuffledQuestions.length - 1; + final isLastQuestion = + _currentQuestionIndex == _shuffledQuestions.length - 1; return QuizQuestionScreen( title: currentQuestion.title, @@ -397,23 +647,24 @@ class _QuizRandomScreenState extends State { currentScore: _currentScore, nextRoute: (context, nextScore) { _nextQuestion(nextScore - _currentScore); - return MaterialPageRoute( - builder: (_) => const SizedBox.shrink(), - ); + return MaterialPageRoute(builder: (_) => const SizedBox.shrink()); }, isFinal: isLastQuestion, showBackButton: _currentQuestionIndex > 0, - onFinished: isLastQuestion ? () { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => QuizResultScreen( - finalScore: _currentScore, - maxScore: 75, - scopeId: widget.scopeId, - ), - ), - ); - } : null, + answerType: currentQuestion.answerType, + onFinished: isLastQuestion + ? () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => QuizResultScreen( + finalScore: _currentScore, + maxScore: 75, + scopeId: widget.scopeId, + ), + ), + ); + } + : null, ); } } @@ -423,11 +674,13 @@ class QuizQuestion { final String title; final String question; final List answers; + final QuizAnswerType answerType; QuizQuestion({ required this.id, required this.title, required this.question, required this.answers, + this.answerType = QuizAnswerType.text, }); } diff --git a/lib/quiz/quiz_result.dart b/lib/quiz/quiz_result.dart index ce5da9c..6f89672 100644 --- a/lib/quiz/quiz_result.dart +++ b/lib/quiz/quiz_result.dart @@ -6,7 +6,12 @@ import 'dart:async'; import 'quiz_prefs.dart'; class QuizResultScreen extends StatefulWidget { - const QuizResultScreen({super.key, required this.finalScore, required this.maxScore, this.scopeId}); + const QuizResultScreen({ + super.key, + required this.finalScore, + required this.maxScore, + this.scopeId, + }); final int finalScore; final int maxScore; @@ -17,39 +22,57 @@ class QuizResultScreen extends StatefulWidget { } class _QuizResultScreenState extends State { + late final Future _saveResultFuture; + @override void initState() { super.initState(); + _saveResultFuture = _saveResult(); + } + + Future _saveResult() async { QuizPrefs.markQuizSeen(); final scope = (widget.scopeId ?? '').trim(); if (scope.isNotEmpty) { - QuizPrefs.saveLastResultForScope(scopeId: scope, score: widget.finalScore, maxScore: widget.maxScore); + await QuizPrefs.saveLastResultForScope( + scopeId: scope, + score: widget.finalScore, + maxScore: widget.maxScore, + ); } else { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid != null && uid.trim().isNotEmpty) { - QuizPrefs.saveLastResultForUser(userId: uid, score: widget.finalScore, maxScore: widget.maxScore); + await QuizPrefs.saveLastResultForUser( + userId: uid, + score: widget.finalScore, + maxScore: widget.maxScore, + ); } else { - QuizPrefs.saveLastResult(score: widget.finalScore, maxScore: widget.maxScore); + await QuizPrefs.saveLastResult( + score: widget.finalScore, + maxScore: widget.maxScore, + ); } } final uid = FirebaseAuth.instance.currentUser?.uid; final userId = (uid ?? '').trim(); - if (userId.isNotEmpty && scope.isNotEmpty && scope.startsWith('${userId}_')) { + if (userId.isNotEmpty && + scope.isNotEmpty && + scope.startsWith('${userId}_')) { final childId = scope.substring(userId.length + 1).trim(); if (childId.isNotEmpty) { - unawaited( - FirebaseFirestore.instance - .collection('users') - .doc(userId) - .collection('children') - .doc(childId) - .set({ - 'lastScore': widget.finalScore, - 'lastMaxScore': widget.maxScore, - 'lastQuizAt': FieldValue.serverTimestamp(), - }, SetOptions(merge: true)).catchError((_) {}), - ); + await FirebaseFirestore.instance + .collection('users') + .doc(userId) + .collection('children') + .doc(childId) + .set({ + 'lastScore': widget.finalScore, + 'lastMaxScore': widget.maxScore, + 'lastQuizAt': FieldValue.serverTimestamp(), + }, SetOptions(merge: true)) + .catchError((_) {}); } } } @@ -66,10 +89,7 @@ class _QuizResultScreenState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFE6F1), - Color(0xFFFFC9DF), - ], + colors: [Color(0xFFFFE6F1), Color(0xFFFFC9DF)], ), ), child: SafeArea( @@ -84,7 +104,8 @@ class _QuizResultScreenState extends State { Align( alignment: Alignment.centerRight, child: TextButton( - onPressed: () => Navigator.of(context).popUntil((r) => r.isFirst), + onPressed: () => + Navigator.of(context).popUntil((r) => r.isFirst), child: const Text(''), ), ), @@ -118,8 +139,12 @@ class _QuizResultScreenState extends State { child: CircularProgressIndicator( value: progress, strokeWidth: 12, - backgroundColor: Colors.black.withValues(alpha: 0.10), - valueColor: const AlwaysStoppedAnimation(Color(0xFF2F9E94)), + backgroundColor: Colors.black + .withValues(alpha: 0.10), + valueColor: + const AlwaysStoppedAnimation( + Color(0xFF2F9E94), + ), ), ), Column( @@ -137,7 +162,9 @@ class _QuizResultScreenState extends State { Text( '${clamped.toInt()}/${widget.maxScore}', style: TextStyle( - color: Colors.black.withValues(alpha: 0.60), + color: Colors.black.withValues( + alpha: 0.60, + ), fontWeight: FontWeight.w800, ), ), @@ -171,7 +198,9 @@ class _QuizResultScreenState extends State { child: Text( 'Descarregar relatório (em breve)', style: TextStyle( - color: const Color(0xFFFF55A7).withValues(alpha: 0.95), + color: const Color( + 0xFFFF55A7, + ).withValues(alpha: 0.95), fontWeight: FontWeight.w800, ), ), @@ -189,9 +218,12 @@ class _QuizResultScreenState extends State { backgroundColor: const Color(0xFF2F9E94), foregroundColor: Colors.white, shape: const StadiumBorder(), - textStyle: const TextStyle(fontWeight: FontWeight.w900), + textStyle: const TextStyle( + fontWeight: FontWeight.w900, + ), ), - onPressed: () => Navigator.of(context).popUntil((r) => r.isFirst), + onPressed: () => + Navigator.of(context).popUntil((r) => r.isFirst), child: const Text('Avançar'), ), ),