Clinicas Parceiras e curiosidades retiradas.

This commit is contained in:
Carlos Correia
2026-05-22 11:10:49 +01:00
parent 4f57044196
commit ea009af0d3
6 changed files with 1809 additions and 1280 deletions

View File

@@ -33,10 +33,7 @@ class _HomeScreenState extends State<HomeScreen> {
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<HomeScreen> {
),
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<HomeScreen> {
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<HomeScreen> {
}
}
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<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;
},
),
),
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<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: onPressed,
child: Text(label),
),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,25 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
typedef QuizNextBuilder = Route<void> Function(BuildContext context, int nextScore);
typedef QuizNextBuilder =
Route<void> 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<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
@@ -41,11 +54,30 @@ class QuizQuestionScreen extends StatefulWidget {
class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
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<QuizQuestionScreen> {
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<QuizQuestionScreen> {
),
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<QuizQuestionScreen> {
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<QuizQuestionScreen> {
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<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;
},
),
),
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: !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<QuizQuestionScreen> {
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<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(),
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<QuizQuestionScreen> {
),
);
}
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,

View File

@@ -19,331 +19,580 @@ class _QuizRandomScreenState extends State<QuizRandomScreen> {
final List<QuizQuestion> _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: '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<QuizRandomScreen> {
}
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<QuizRandomScreen> {
currentScore: _currentScore,
nextRoute: (context, nextScore) {
_nextQuestion(nextScore - _currentScore);
return MaterialPageRoute<void>(
builder: (_) => const SizedBox.shrink(),
);
return MaterialPageRoute<void>(builder: (_) => const SizedBox.shrink());
},
isFinal: isLastQuestion,
showBackButton: _currentQuestionIndex > 0,
onFinished: isLastQuestion ? () {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => QuizResultScreen(
finalScore: _currentScore,
maxScore: 75,
scopeId: widget.scopeId,
),
),
);
} : null,
answerType: currentQuestion.answerType,
onFinished: isLastQuestion
? () {
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (_) => QuizResultScreen(
finalScore: _currentScore,
maxScore: 75,
scopeId: widget.scopeId,
),
),
);
}
: null,
);
}
}
@@ -423,11 +674,13 @@ class QuizQuestion {
final String title;
final String question;
final List<QuizAnswer> answers;
final QuizAnswerType answerType;
QuizQuestion({
required this.id,
required this.title,
required this.question,
required this.answers,
this.answerType = QuizAnswerType.text,
});
}

View File

@@ -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<QuizResultScreen> {
late final Future<void> _saveResultFuture;
@override
void initState() {
super.initState();
_saveResultFuture = _saveResult();
}
Future<void> _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<QuizResultScreen> {
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<QuizResultScreen> {
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<QuizResultScreen> {
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<QuizResultScreen> {
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<QuizResultScreen> {
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<QuizResultScreen> {
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'),
),
),