Documentação
This commit is contained in:
53
lib/auth_gate.dart
Normal file
53
lib/auth_gate.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'home_screen.dart';
|
||||
import 'logged_home.dart';
|
||||
|
||||
final ValueNotifier<bool> forceHomeScreen = ValueNotifier<bool>(false);
|
||||
|
||||
class AuthGate extends StatelessWidget {
|
||||
const AuthGate({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: forceHomeScreen,
|
||||
builder: (context, forcedHome, _) {
|
||||
return StreamBuilder<User?>(
|
||||
stream: FirebaseAuth.instance.authStateChanges(),
|
||||
builder: (context, snapshot) {
|
||||
final user = snapshot.data;
|
||||
|
||||
final Widget child;
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
child = const SizedBox.shrink();
|
||||
} else if (forcedHome || user == null) {
|
||||
child = const HomeScreen(key: ValueKey('home_screen'));
|
||||
} else {
|
||||
child = const LoggedHomeScreen(key: ValueKey('logged_home_screen'));
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 280),
|
||||
reverseDuration: const Duration(milliseconds: 240),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) {
|
||||
final fade = CurvedAnimation(parent: animation, curve: Curves.easeOut);
|
||||
return FadeTransition(
|
||||
opacity: fade,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.985, end: 1.0).animate(fade),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/gates/debug_launch_gate.dart
Normal file
69
lib/gates/debug_launch_gate.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../auth_gate.dart';
|
||||
import '../screens/hello_splash_screen.dart';
|
||||
|
||||
class DebugLaunchGate extends StatefulWidget {
|
||||
const DebugLaunchGate({super.key});
|
||||
|
||||
@override
|
||||
State<DebugLaunchGate> createState() => _DebugLaunchGateState();
|
||||
}
|
||||
|
||||
class _DebugLaunchGateState extends State<DebugLaunchGate> {
|
||||
bool _showHello = true;
|
||||
bool _shouldLaunchQuiz = false;
|
||||
bool _quizLaunched = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadQuizFlag();
|
||||
}
|
||||
|
||||
Future<void> _loadQuizFlag() async {
|
||||
// O quiz NÃO deve iniciar automaticamente na primeira abertura.
|
||||
// Ele deve iniciar apenas quando o usuário clicar no botão "Iniciar Quiz"
|
||||
// ou quando houver um registro novo (fluxo tratado no LoggedHome).
|
||||
if (!mounted) return;
|
||||
setState(() => _shouldLaunchQuiz = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget child;
|
||||
if (_showHello) {
|
||||
child = HelloSplashScreen(
|
||||
key: const ValueKey('hello_splash'),
|
||||
onFinished: () {
|
||||
if (!mounted) return;
|
||||
setState(() => _showHello = false);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = const AuthGate(key: ValueKey('auth_gate'));
|
||||
}
|
||||
|
||||
if (!_showHello && _shouldLaunchQuiz && !_quizLaunched) {
|
||||
_quizLaunched = true;
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 420),
|
||||
reverseDuration: const Duration(milliseconds: 260),
|
||||
switchInCurve: Curves.easeOutCubic,
|
||||
switchOutCurve: Curves.easeInCubic,
|
||||
transitionBuilder: (child, animation) {
|
||||
final fade = CurvedAnimation(parent: animation, curve: Curves.easeOut);
|
||||
return FadeTransition(
|
||||
opacity: fade,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.995, end: 1.0).animate(fade),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
190
lib/home_screen.dart
Normal file
190
lib/home_screen.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
import 'login_register/login_sheet.dart';
|
||||
import 'login_register/register_sheet.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
bool _paused = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: _paused,
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.38,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'Check-Teeth Kids',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Color(0xFFFF55A7),
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 22),
|
||||
SizedBox(
|
||||
width: size.width * 0.78,
|
||||
child: _PrimaryButton(
|
||||
label: 'Cadastrar',
|
||||
onPressed: _openRegister,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(
|
||||
width: size.width * 0.78,
|
||||
child: _PrimaryButton(
|
||||
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.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openLogin() async {
|
||||
setState(() => _paused = true);
|
||||
try {
|
||||
await showLoginSheet(context);
|
||||
} finally {
|
||||
if (mounted) setState(() => _paused = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openRegister() async {
|
||||
setState(() => _paused = true);
|
||||
try {
|
||||
await showRegisterSheet(context);
|
||||
} finally {
|
||||
if (mounted) setState(() => _paused = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PrimaryButton extends StatelessWidget {
|
||||
const _PrimaryButton({required this.label, required this.onPressed});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color teal = const Color(0xFF2F9E94);
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1832
lib/logged_home.dart
Normal file
1832
lib/logged_home.dart
Normal file
File diff suppressed because it is too large
Load Diff
281
lib/login_register/login_sheet.dart
Normal file
281
lib/login_register/login_sheet.dart
Normal file
@@ -0,0 +1,281 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
Future<void> showLoginSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) => const _AnimatedAuthSheet(child: LoginBottomSheet()),
|
||||
);
|
||||
}
|
||||
|
||||
class _AnimatedAuthSheet extends StatelessWidget {
|
||||
const _AnimatedAuthSheet({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
const topRadius = Radius.circular(20);
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 260),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, t, w) {
|
||||
return Opacity(
|
||||
opacity: t,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, (1 - t) * 12),
|
||||
child: w,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: topRadius),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 28 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LoginBottomSheet extends StatefulWidget {
|
||||
const LoginBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<LoginBottomSheet> createState() => _LoginBottomSheetState();
|
||||
}
|
||||
|
||||
class _LoginBottomSheetState extends State<LoginBottomSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewInsets = MediaQuery.viewInsetsOf(context);
|
||||
const accentPink = Color(0xFFFF55A7);
|
||||
const primaryTeal = Color(0xFF2F9E94);
|
||||
final underlineBorder = UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.20)),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 16 + viewInsets.bottom,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 520),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 46,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'Entrar',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: accentPink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe seu email';
|
||||
if (!value.contains('@')) return 'Email inválido';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Senha',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final value = (v ?? '');
|
||||
if (value.isEmpty) return 'Informe sua senha';
|
||||
if (value.length < 6) return 'Mínimo de 6 caracteres';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 46,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryTeal,
|
||||
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
onPressed: _loading ? null : _submit,
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Entrar'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _loading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Fechar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text;
|
||||
|
||||
await FirebaseAuth.instance.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Login efetuado')),
|
||||
);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(_friendlyAuthError(e))),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erro: $e')),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendlyAuthError(FirebaseAuthException e) {
|
||||
switch (e.code) {
|
||||
case 'invalid-email':
|
||||
return 'Email inválido.';
|
||||
case 'user-disabled':
|
||||
return 'Usuário desativado.';
|
||||
case 'user-not-found':
|
||||
case 'wrong-password':
|
||||
case 'invalid-credential':
|
||||
return 'Email ou senha incorretos.';
|
||||
default:
|
||||
return e.message ?? 'Falha de autenticação.';
|
||||
}
|
||||
}
|
||||
}
|
||||
451
lib/login_register/register_sheet.dart
Normal file
451
lib/login_register/register_sheet.dart
Normal file
@@ -0,0 +1,451 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
Future<void> showRegisterSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (ctx) => const _AnimatedAuthSheet(child: RegisterBottomSheet()),
|
||||
);
|
||||
}
|
||||
|
||||
class _AnimatedAuthSheet extends StatelessWidget {
|
||||
const _AnimatedAuthSheet({required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
const topRadius = Radius.circular(20);
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(begin: 0.0, end: 1.0),
|
||||
duration: const Duration(milliseconds: 260),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, t, w) {
|
||||
return Opacity(
|
||||
opacity: t,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, (1 - t) * 12),
|
||||
child: w,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: topRadius),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.38,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 28 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterBottomSheet extends StatefulWidget {
|
||||
const RegisterBottomSheet({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterBottomSheet> createState() => _RegisterBottomSheetState();
|
||||
}
|
||||
|
||||
class _RegisterBottomSheetState extends State<RegisterBottomSheet> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
final _childNameController = TextEditingController();
|
||||
final _childAgeController = TextEditingController();
|
||||
String? _childGender;
|
||||
|
||||
bool _loading = false;
|
||||
|
||||
static const String _kPendingQuizScopeKey = 'pending_quiz_scope_v1';
|
||||
|
||||
Future<void> _persistRegistrationData({
|
||||
required String uid,
|
||||
required String name,
|
||||
required String email,
|
||||
required String childId,
|
||||
required String childName,
|
||||
required int childAge,
|
||||
required String childGender,
|
||||
}) async {
|
||||
await Future.wait([
|
||||
FirebaseFirestore.instance.collection('users').doc(uid).set({
|
||||
'name': name,
|
||||
'email': email,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
}, SetOptions(merge: true)).timeout(const Duration(seconds: 20)),
|
||||
FirebaseFirestore.instance
|
||||
.collection('users')
|
||||
.doc(uid)
|
||||
.collection('children')
|
||||
.doc(childId)
|
||||
.set({
|
||||
'id': childId,
|
||||
'name': childName,
|
||||
'age': childAge,
|
||||
'gender': childGender,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
}, SetOptions(merge: true)).timeout(const Duration(seconds: 20)),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_childNameController.dispose();
|
||||
_childAgeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewInsets = MediaQuery.viewInsetsOf(context);
|
||||
const accentPink = Color(0xFFFF55A7);
|
||||
const primaryTeal = Color(0xFF2F9E94);
|
||||
final underlineBorder = UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black.withValues(alpha: 0.20)),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 16 + viewInsets.bottom,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 560),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 46,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Text(
|
||||
'Criar conta',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: accentPink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nome',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Informe seu nome';
|
||||
if (v.trim().length < 2) return 'Nome muito curto';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe seu email';
|
||||
if (!value.contains('@')) return 'Email inválido';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Senha',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final value = (v ?? '');
|
||||
if (value.isEmpty) return 'Informe sua senha';
|
||||
if (value.length < 6) return 'Mínimo de 6 caracteres';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
TextFormField(
|
||||
controller: _childNameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nome do filho(a)',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final value = (v ?? '').trim();
|
||||
if (value.isEmpty) return 'Informe o nome do filho(a)';
|
||||
if (value.length < 2) return 'Nome muito curto';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _childAgeController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Idade do filho(a)',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
final raw = (v ?? '').trim();
|
||||
if (raw.isEmpty) return 'Informe a idade do filho(a)';
|
||||
final age = int.tryParse(raw);
|
||||
if (age == null) return 'Idade inválida';
|
||||
if (age < 0 || age > 25) return 'Idade inválida';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _childGender,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Masculino', child: Text('Masculino')),
|
||||
DropdownMenuItem(value: 'Feminino', child: Text('Feminino')),
|
||||
DropdownMenuItem(value: 'Outro', child: Text('Outro')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _childGender = v),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Gênero do filho(a)',
|
||||
border: underlineBorder,
|
||||
enabledBorder: underlineBorder,
|
||||
focusedBorder: underlineBorder.copyWith(
|
||||
borderSide: const BorderSide(color: primaryTeal, width: 1.6),
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(color: primaryTeal, fontWeight: FontWeight.w700),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return 'Selecione o gênero';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 46,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: primaryTeal,
|
||||
foregroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
onPressed: _loading ? null : _submit,
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Registrar'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _loading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Fechar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!(_formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final name = _nameController.text.trim();
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text;
|
||||
|
||||
final childName = _childNameController.text.trim();
|
||||
final childAge = int.parse(_childAgeController.text.trim());
|
||||
final childGender = (_childGender ?? '').trim();
|
||||
|
||||
final credential = await FirebaseAuth.instance
|
||||
.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
)
|
||||
.timeout(const Duration(seconds: 20));
|
||||
|
||||
final user = credential.user;
|
||||
if (user == null) {
|
||||
throw StateError('Usuário não encontrado após criar conta.');
|
||||
}
|
||||
|
||||
final uid = user.uid;
|
||||
|
||||
// Gera o childId antes de fechar o sheet para termos um scopeId determinístico.
|
||||
final childId = FirebaseFirestore.instance
|
||||
.collection('users')
|
||||
.doc(uid)
|
||||
.collection('children')
|
||||
.doc()
|
||||
.id;
|
||||
final scopeId = '${uid}_$childId';
|
||||
|
||||
// Marca para o LoggedHome abrir automaticamente o quiz desta criança.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_kPendingQuizScopeKey, scopeId);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Fecha o sheet imediatamente após autenticar.
|
||||
// As gravações no Firestore seguem em background para não travar a UI.
|
||||
Navigator.of(context).pop();
|
||||
|
||||
unawaited(
|
||||
_persistRegistrationData(
|
||||
uid: uid,
|
||||
name: name,
|
||||
email: email,
|
||||
childId: childId,
|
||||
childName: childName,
|
||||
childAge: childAge,
|
||||
childGender: childGender,
|
||||
).catchError((_) {}),
|
||||
);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(_friendlyAuthError(e))),
|
||||
);
|
||||
} on TimeoutException {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Tempo esgotado. Verifique sua conexão e tente novamente.')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erro: $e')),
|
||||
);
|
||||
} finally {
|
||||
if (mounted && _loading) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
String _friendlyAuthError(FirebaseAuthException e) {
|
||||
switch (e.code) {
|
||||
case 'invalid-email':
|
||||
return 'Email inválido.';
|
||||
case 'email-already-in-use':
|
||||
return 'Este email já está em uso.';
|
||||
case 'weak-password':
|
||||
return 'Senha fraca. Use pelo menos 6 caracteres.';
|
||||
default:
|
||||
return e.message ?? 'Falha de autenticação.';
|
||||
}
|
||||
}
|
||||
}
|
||||
42
lib/main.dart
Normal file
42
lib/main.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'gates/debug_launch_gate.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.current);
|
||||
};
|
||||
|
||||
runZonedGuarded(() async {
|
||||
await Firebase.initializeApp();
|
||||
runApp(const MyApp());
|
||||
}, (error, stack) {
|
||||
debugPrint('UNCAUGHT: $error');
|
||||
debugPrintStack(stackTrace: stack);
|
||||
});
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Check-Teeth Kids',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2F9E94)),
|
||||
scaffoldBackgroundColor: const Color(0xFFFFE2EF),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const DebugLaunchGate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/quiz/README_QUIZ.md
Normal file
140
lib/quiz/README_QUIZ.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Sistema de Quiz Extendido - Check-Teeth Kids
|
||||
|
||||
## Novos Arquivos Criados
|
||||
|
||||
### 1. `quiz_extended.dart`
|
||||
Contém 15 novas telas de quiz sequenciais (Quiz 6-20) com temas educativos sobre saúde bucal:
|
||||
|
||||
- **Quiz 6**: Tipos de escova para crianças
|
||||
- **Quiz 7**: Alimentos que causam cáries
|
||||
- **Quiz 8**: Primeira visita ao dentista
|
||||
- **Quiz 9**: Uso de chupeta
|
||||
- **Quiz 10**: Flúor na água
|
||||
- **Quiz 11**: Escovação noturna
|
||||
- **Quiz 12**: Bebidas ácidas
|
||||
- **Quiz 13**: Importância dos dentes de leite
|
||||
- **Quiz 14**: Técnica de escovação
|
||||
- **Quiz 15**: Enxaguante bucal infantil
|
||||
- **Quiz 16**: Lanches escolares saudáveis
|
||||
- **Quiz 17**: Traumas dentários
|
||||
- **Quiz 18**: Problemas na mordida
|
||||
- **Quiz 19**: Gengivas sangrando
|
||||
- **Quiz 20**: Selantes dentários
|
||||
|
||||
### 2. `quiz_random.dart`
|
||||
Sistema de quiz aleatório com 15 perguntas selecionadas aleatoriamente a cada sessão.
|
||||
|
||||
## Como Usar
|
||||
|
||||
### Para Quiz Sequencial Extendido (20 perguntas):
|
||||
```dart
|
||||
import 'quiz_extended.dart';
|
||||
|
||||
// Para iniciar do Quiz 6:
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const Quiz6Screen()),
|
||||
);
|
||||
|
||||
// Para conectar ao final do Quiz 5, modifique quiz5.dart:
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
```
|
||||
|
||||
### Para Quiz Aleatório (15 perguntas):
|
||||
```dart
|
||||
import 'quiz_random.dart';
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const QuizRandomScreen()),
|
||||
);
|
||||
```
|
||||
|
||||
## Sistema de Pontuação
|
||||
|
||||
- **Quiz Sequencial**: 20 perguntas × 5 pontos = máximo 100 pontos
|
||||
- **Quiz Aleatório**: 15 perguntas × 5 pontos = máximo 75 pontos
|
||||
- **Sistema de pesos**: 2 (melhor) a 5 (pior) pontos
|
||||
|
||||
## Estrutura das Perguntas
|
||||
|
||||
Cada quiz segue o padrão:
|
||||
```dart
|
||||
QuizQuestionScreen(
|
||||
title: 'Quiz X/20',
|
||||
question: 'Pergunta educativa...',
|
||||
answers: [
|
||||
QuizAnswer(title: 'Resposta A', description: 'Explicação...', weight: 2),
|
||||
QuizAnswer(title: 'Resposta B', description: 'Explicação...', weight: 5),
|
||||
QuizAnswer(title: 'Resposta C', description: 'Explicação...', weight: 3),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute(...),
|
||||
showBackButton: true,
|
||||
);
|
||||
```
|
||||
|
||||
## Temas Abordados
|
||||
|
||||
### 🦷 Higiene Oral
|
||||
- Tempo e técnica de escovação
|
||||
- Tipos de escova e pasta de dente
|
||||
- Uso de fio dental e enxaguante
|
||||
|
||||
### 🍎 Nutrição e Saúde
|
||||
- Alimentos prejudiciais e benéficos
|
||||
- Bebidas ácidas vs neutras
|
||||
- Lanches escolares saudáveis
|
||||
|
||||
### 👶 Desenvolvimento Infantil
|
||||
- Dentes de leite e permanentes
|
||||
- Hábitos como chupeta e sucção
|
||||
- Primeira visita ao dentista
|
||||
|
||||
### 🔬 Prevenção e Tratamento
|
||||
- Flúor e selantes
|
||||
- Traumas dentários
|
||||
- Problemas gengivais
|
||||
|
||||
## Integração com Sistema Existente
|
||||
|
||||
Os novos quizzes são totalmente compatíveis com:
|
||||
- ✅ Sistema de pontuação existente
|
||||
- ✅ Tela de resultados (`QuizResultScreen`)
|
||||
- ✅ Navegação e animações
|
||||
- ✅ Design e cores do app
|
||||
- ✅ Firebase (scopeId)
|
||||
|
||||
## Personalização
|
||||
|
||||
Para modificar o quiz aleatório:
|
||||
```dart
|
||||
// Em quiz_random.dart, altere o número de perguntas:
|
||||
final List<QuizQuestion> _selectedQuestions = _allQuestions.take(10).toList(); // 10 perguntas
|
||||
```
|
||||
|
||||
Para adicionar novas perguntas:
|
||||
```dart
|
||||
// Adicione ao final da lista _allQuestions em quiz_random.dart
|
||||
QuizQuestion(
|
||||
id: 16,
|
||||
title: 'Quiz 16/15',
|
||||
question: 'Nova pergunta...',
|
||||
answers: [...],
|
||||
),
|
||||
```
|
||||
|
||||
## Teste e Validação
|
||||
|
||||
Os arquivos foram testados com:
|
||||
- ✅ `flutter analyze` - sem erros
|
||||
- ✅ Estrutura compatível com código existente
|
||||
- ✅ Importações corretas
|
||||
- ✅ Navegação funcional
|
||||
|
||||
---
|
||||
|
||||
*Criado em 01/05/2026*
|
||||
*Total de perguntas: 35 (5 originais + 15 sequenciais + 15 aleatórias)*
|
||||
0
lib/quiz/quiz.dart
Normal file
0
lib/quiz/quiz.dart
Normal file
825
lib/quiz/quiz1.dart
Normal file
825
lib/quiz/quiz1.dart
Normal file
@@ -0,0 +1,825 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz_question_screen.dart';
|
||||
import 'quiz_result.dart';
|
||||
|
||||
// Quiz 1: Tipos de Escova (antiga Quiz 6)
|
||||
class Quiz1Screen extends StatelessWidget {
|
||||
const Quiz1Screen({super.key, this.currentScore = 0, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 1/20',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 2: Alimentos que Causam Cáries (antiga Quiz 7)
|
||||
class Quiz2Screen extends StatelessWidget {
|
||||
const Quiz2Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 2/20',
|
||||
question: 'Qual alimento é mais prejudicial para os dentes?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Balas e chicletes pegajosos',
|
||||
description:
|
||||
'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Frutas frescas',
|
||||
description:
|
||||
'Frutas são saudáveis, mas algumas são ácidas. O problema maior são os alimentos açucarados e pegajosos.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Vegetais crus',
|
||||
description:
|
||||
'Vegetais são geralmente seguros para os dentes e muitos ajudam na limpeza natural.',
|
||||
weight: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 3: Primeira Visita ao Dentista (antiga Quiz 8)
|
||||
class Quiz3Screen extends StatelessWidget {
|
||||
const Quiz3Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 3/20',
|
||||
question: 'Qual a idade ideal para a primeira visita ao dentista?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'A partir dos 1 ano de idade',
|
||||
description:
|
||||
'O recomendado é levar ao dentista assim que o primeiro dentição aparecer ou até 1 ano.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só depois dos 6 anos',
|
||||
description:
|
||||
'Esperar demais pode permitir que problemas sérios se desenvolvam sem detecção precoce.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Apenas quando houver dor',
|
||||
description:
|
||||
'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 4: Uso de Fio Dental (antiga Quiz 9)
|
||||
class Quiz4Screen extends StatelessWidget {
|
||||
const Quiz4Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 4/20',
|
||||
question: 'Com que frequência crianças devem usar fio dental?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Pelo menos uma vez ao dia',
|
||||
description:
|
||||
'O uso diário de fio dental é importante para remover placa entre os dentes onde a escova não alcança.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só quando os dentes estiverem muito juntos',
|
||||
description:
|
||||
'Fio dental é necessário independentemente do espaçamento dos dentes para remover placa bacteriana.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Uma vez por semana é suficiente',
|
||||
description:
|
||||
'Uso semanal é insuficiente. Placa bacteriana se forma diariamente e precisa ser removida.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 5: Flúor (antiga Quiz 10)
|
||||
class Quiz5Screen extends StatelessWidget {
|
||||
const Quiz5Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 6: Chupetas e Mamadeiras (antiga Quiz 11)
|
||||
class Quiz6Screen extends StatelessWidget {
|
||||
const Quiz6Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz7Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 7: Bebidas e Dentição (antiga Quiz 12)
|
||||
class Quiz7Screen extends StatelessWidget {
|
||||
const Quiz7Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 7/20',
|
||||
question: 'Qual bebida é mais prejudicial para os dentes das crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Refrigerantes e sucos industrializados',
|
||||
description:
|
||||
'Bebidas açucaradas e ácidas são as principais causas de cáries infantis, especialmente se consumidas frequentemente.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Leite e água',
|
||||
description:
|
||||
'Leite e água são seguros para os dentes. O problema são bebidas açucaradas e ácidas.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Sucos naturais sem açúcar',
|
||||
description:
|
||||
'Sucos naturais são melhores que industrializados, mas alguns são ácidos. Moderação é importante.',
|
||||
weight: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 8: Hábitos Noturnos (antiga Quiz 13)
|
||||
class Quiz8Screen extends StatelessWidget {
|
||||
const Quiz8Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 8/20',
|
||||
question: 'Crianças devem escovar os dentes antes de dormir?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Sim, é fundamental antes de dormir',
|
||||
description:
|
||||
'Escovação noturna é crucial porque durante a noite a produção de saliva diminui, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só se comeu doce',
|
||||
description:
|
||||
'Placa bacteriana se acumula durante o dia independentemente do que foi comido. Escovação noturna é sempre necessária.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não precisa se escovou durante o dia',
|
||||
description:
|
||||
'Mesmo com escovação diurna, a noturna é essencial devido à redução de saliva durante o sono.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 9: Traumatismos Dentários (antiga Quiz 14)
|
||||
class Quiz9Screen extends StatelessWidget {
|
||||
const Quiz9Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 9/20',
|
||||
question: 'O que fazer se uma criança cair e quebrar um dente?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Procurar dentista imediatamente',
|
||||
description:
|
||||
'Traumatismo dentário é emergência. Quanto mais rápido o atendimento, melhor o prognóstico.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz10Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 10: Selantes (antiga Quiz 15)
|
||||
class Quiz10Screen extends StatelessWidget {
|
||||
const Quiz10Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 10/20',
|
||||
question: 'Para que servem os selantes dentários?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz11Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 11: Aparelhos Ortodônticos (antiga Quiz 16)
|
||||
class Quiz11Screen extends StatelessWidget {
|
||||
const Quiz11Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 11/20',
|
||||
question: 'Qual a melhor idade para avaliar necessidade de aparelho?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz12Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 12: Respiração Bucal (antiga Quiz 17)
|
||||
class Quiz12Screen extends StatelessWidget {
|
||||
const Quiz12Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 12/20',
|
||||
question: 'Respirar pela boca afeta os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz13Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 13: Gengivas (antiga Quiz 18)
|
||||
class Quiz13Screen extends StatelessWidget {
|
||||
const Quiz13Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 13/20',
|
||||
question: 'O que causa gengivas inflamadas em crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Higiene inadequada e acúmulo de placa',
|
||||
description:
|
||||
'Placa bacteriana não removida properly causa inflamação gengival, a forma mais comum de gengivite.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz14Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 14: Lanche Escolar (antiga Quiz 19)
|
||||
class Quiz14Screen extends StatelessWidget {
|
||||
const Quiz14Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 14/20',
|
||||
question: 'Qual lanche escolar é melhor para os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz15Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 15: Medo do Dentista (antiga Quiz 20)
|
||||
class Quiz15Screen extends StatelessWidget {
|
||||
const Quiz15Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 15/20',
|
||||
question: 'Como lidar com o medo do dentista em crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Conversar positivamente e visitar regularmente',
|
||||
description:
|
||||
'Linguagem positiva e visitas frequentes sem necessidade de tratamento ajudam a criar confiança.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz16Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 16: Tempo ideal para escovar (antiga Quiz 1)
|
||||
class Quiz16Screen extends StatelessWidget {
|
||||
const Quiz16Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 16/20',
|
||||
question: 'Qual é o tempo ideal para escovar os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz17Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 17: Troca da escova (antiga Quiz 2)
|
||||
class Quiz17Screen extends StatelessWidget {
|
||||
const Quiz17Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 17/20',
|
||||
question: 'Quando devo trocar a escova de dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz18Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 18: Quantidade de pasta (antiga Quiz 3)
|
||||
class Quiz18Screen extends StatelessWidget {
|
||||
const Quiz18Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 18/20',
|
||||
question: 'Qual a quantidade ideal de pasta de dente para crianças?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz19Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 19: Fio dental (antiga Quiz 4)
|
||||
class Quiz19Screen extends StatelessWidget {
|
||||
const Quiz19Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 19/20',
|
||||
question: 'Qual é o melhor horário para usar fio dental?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz20Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 20: Prevenção de cáries (antiga Quiz 5)
|
||||
class Quiz20Screen extends StatelessWidget {
|
||||
const Quiz20Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 20/20',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 100, scopeId: scopeId),
|
||||
),
|
||||
isFinal: true,
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lib/quiz/quiz2.dart
Normal file
46
lib/quiz/quiz2.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz3.dart';
|
||||
import 'quiz_question_screen.dart';
|
||||
|
||||
class Quiz2Screen extends StatelessWidget {
|
||||
const Quiz2Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 2/5',
|
||||
question: 'Quando devo trocar a escova de dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
46
lib/quiz/quiz3.dart
Normal file
46
lib/quiz/quiz3.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz4.dart';
|
||||
import 'quiz_question_screen.dart';
|
||||
|
||||
class Quiz3Screen extends StatelessWidget {
|
||||
const Quiz3Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 3/5',
|
||||
question: 'Qual a quantidade ideal de pasta de dente para crianças?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
46
lib/quiz/quiz4.dart
Normal file
46
lib/quiz/quiz4.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz5.dart';
|
||||
import 'quiz_question_screen.dart';
|
||||
|
||||
class Quiz4Screen extends StatelessWidget {
|
||||
const Quiz4Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 4/5',
|
||||
question: 'Qual é o melhor horário para usar fio dental?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
46
lib/quiz/quiz5.dart
Normal file
46
lib/quiz/quiz5.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz_result.dart';
|
||||
import 'quiz_question_screen.dart';
|
||||
|
||||
class Quiz5Screen extends StatelessWidget {
|
||||
const Quiz5Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 5/5',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 25, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
825
lib/quiz/quiz_complete.dart
Normal file
825
lib/quiz/quiz_complete.dart
Normal file
@@ -0,0 +1,825 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz_question_screen.dart';
|
||||
import 'quiz_result.dart';
|
||||
|
||||
// Quiz 1: Tipos de Escova (antiga Quiz 6)
|
||||
class Quiz1Screen extends StatelessWidget {
|
||||
const Quiz1Screen({super.key, this.currentScore = 0, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 1/20',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz2Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 2: Alimentos que Causam Cáries (antiga Quiz 7)
|
||||
class Quiz2Screen extends StatelessWidget {
|
||||
const Quiz2Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 2/20',
|
||||
question: 'Qual alimento é mais prejudicial para os dentes?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Balas e chicletes pegajosos',
|
||||
description:
|
||||
'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Frutas frescas',
|
||||
description:
|
||||
'Frutas são saudáveis, mas algumas são ácidas. O problema maior são os alimentos açucarados e pegajosos.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Vegetais crus',
|
||||
description:
|
||||
'Vegetais são geralmente seguros para os dentes e muitos ajudam na limpeza natural.',
|
||||
weight: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz3Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 3: Primeira Visita ao Dentista (antiga Quiz 8)
|
||||
class Quiz3Screen extends StatelessWidget {
|
||||
const Quiz3Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 3/20',
|
||||
question: 'Qual a idade ideal para a primeira visita ao dentista?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'A partir dos 1 ano de idade',
|
||||
description:
|
||||
'O recomendado é levar ao dentista assim que o primeiro dentição aparecer ou até 1 ano.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só depois dos 6 anos',
|
||||
description:
|
||||
'Esperar demais pode permitir que problemas sérios se desenvolvam sem detecção precoce.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Apenas quando houver dor',
|
||||
description:
|
||||
'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz4Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 4: Uso de Fio Dental (antiga Quiz 9)
|
||||
class Quiz4Screen extends StatelessWidget {
|
||||
const Quiz4Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 4/20',
|
||||
question: 'Com que frequência crianças devem usar fio dental?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Pelo menos uma vez ao dia',
|
||||
description:
|
||||
'O uso diário de fio dental é importante para remover placa entre os dentes onde a escova não alcança.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só quando os dentes estiverem muito juntos',
|
||||
description:
|
||||
'Fio dental é necessário independentemente do espaçamento dos dentes para remover placa bacteriana.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Uma vez por semana é suficiente',
|
||||
description:
|
||||
'Uso semanal é insuficiente. Placa bacteriana se forma diariamente e precisa ser removida.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz5Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 5: Flúor (antiga Quiz 10)
|
||||
class Quiz5Screen extends StatelessWidget {
|
||||
const Quiz5Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz6Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 6: Chupetas e Mamadeiras (antiga Quiz 11)
|
||||
class Quiz6Screen extends StatelessWidget {
|
||||
const Quiz6Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz7Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 7: Bebidas e Dentição (antiga Quiz 12)
|
||||
class Quiz7Screen extends StatelessWidget {
|
||||
const Quiz7Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 7/20',
|
||||
question: 'Qual bebida é mais prejudicial para os dentes das crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Refrigerantes e sucos industrializados',
|
||||
description:
|
||||
'Bebidas açucaradas e ácidas são as principais causas de cáries infantis, especialmente se consumidas frequentemente.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Leite e água',
|
||||
description:
|
||||
'Leite e água são seguros para os dentes. O problema são bebidas açucaradas e ácidas.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Sucos naturais sem açúcar',
|
||||
description:
|
||||
'Sucos naturais são melhores que industrializados, mas alguns são ácidos. Moderação é importante.',
|
||||
weight: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 8: Hábitos Noturnos (antiga Quiz 13)
|
||||
class Quiz8Screen extends StatelessWidget {
|
||||
const Quiz8Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 8/20',
|
||||
question: 'Crianças devem escovar os dentes antes de dormir?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Sim, é fundamental antes de dormir',
|
||||
description:
|
||||
'Escovação noturna é crucial porque durante a noite a produção de saliva diminui, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só se comeu doce',
|
||||
description:
|
||||
'Placa bacteriana se acumula durante o dia independentemente do que foi comido. Escovação noturna é sempre necessária.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não precisa se escovou durante o dia',
|
||||
description:
|
||||
'Mesmo com escovação diurna, a noturna é essencial devido à redução de saliva durante o sono.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 9: Traumatismos Dentários (antiga Quiz 14)
|
||||
class Quiz9Screen extends StatelessWidget {
|
||||
const Quiz9Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 9/20',
|
||||
question: 'O que fazer se uma criança cair e quebrar um dente?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Procurar dentista imediatamente',
|
||||
description:
|
||||
'Traumatismo dentário é emergência. Quanto mais rápido o atendimento, melhor o prognóstico.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz10Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 10: Selantes (antiga Quiz 15)
|
||||
class Quiz10Screen extends StatelessWidget {
|
||||
const Quiz10Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 10/20',
|
||||
question: 'Para que servem os selantes dentários?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Clarear os dentes',
|
||||
description:
|
||||
'Selantes não têm função estética de clareamento, apenas protetiva 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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz11Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 11: Aparelhos Ortodônticos (antiga Quiz 16)
|
||||
class Quiz11Screen extends StatelessWidget {
|
||||
const Quiz11Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 11/20',
|
||||
question: 'Qual a melhor idade para avaliar necessidade de aparelho?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz12Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 12: Respiração Bucal (antiga Quiz 17)
|
||||
class Quiz12Screen extends StatelessWidget {
|
||||
const Quiz12Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 12/20',
|
||||
question: 'Respirar pela boca afeta os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz13Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 13: Gengivas (antiga Quiz 18)
|
||||
class Quiz13Screen extends StatelessWidget {
|
||||
const Quiz13Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 13/20',
|
||||
question: 'O que causa gengivas inflamadas em crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Higiene inadequada e acúmulo de placa',
|
||||
description:
|
||||
'Placa bacteriana não removida properly causa inflamação gengival, a forma mais comum de gengivite.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz14Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 14: Lanche Escolar (antiga Quiz 19)
|
||||
class Quiz14Screen extends StatelessWidget {
|
||||
const Quiz14Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 14/20',
|
||||
question: 'Qual lanche escolar é melhor para os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz15Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 15: Medo do Dentista (antiga Quiz 20)
|
||||
class Quiz15Screen extends StatelessWidget {
|
||||
const Quiz15Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 15/20',
|
||||
question: 'Como lidar com o medo do dentista em crianças?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Conversar positivamente e visitar regularmente',
|
||||
description:
|
||||
'Linguagem positiva e visitas frequentes sem necessidade de tratamento ajudam a criar confiança.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz16Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 16: Tempo ideal para escovar (antiga Quiz 1)
|
||||
class Quiz16Screen extends StatelessWidget {
|
||||
const Quiz16Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 16/20',
|
||||
question: 'Qual é o tempo ideal para escovar os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz17Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 17: Troca da escova (antiga Quiz 2)
|
||||
class Quiz17Screen extends StatelessWidget {
|
||||
const Quiz17Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 17/20',
|
||||
question: 'Quando devo trocar a escova de dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz18Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 18: Quantidade de pasta (antiga Quiz 3)
|
||||
class Quiz18Screen extends StatelessWidget {
|
||||
const Quiz18Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 18/20',
|
||||
question: 'Qual a quantidade ideal de pasta de dente para crianças?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz19Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 19: Fio dental (antiga Quiz 4)
|
||||
class Quiz19Screen extends StatelessWidget {
|
||||
const Quiz19Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 19/20',
|
||||
question: 'Qual é o melhor horário para usar fio dental?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz20Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 20: Prevenção de cáries (antiga Quiz 5)
|
||||
class Quiz20Screen extends StatelessWidget {
|
||||
const Quiz20Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 20/20',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 100, scopeId: scopeId),
|
||||
),
|
||||
isFinal: true,
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
620
lib/quiz/quiz_extended.dart
Normal file
620
lib/quiz/quiz_extended.dart
Normal file
@@ -0,0 +1,620 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz_question_screen.dart';
|
||||
import 'quiz_result.dart';
|
||||
|
||||
// Quiz 6: Tipos de Escova
|
||||
class Quiz6Screen extends StatelessWidget {
|
||||
const Quiz6Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 6/20',
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz7Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 7: Alimentos que Causam Cáries
|
||||
class Quiz7Screen extends StatelessWidget {
|
||||
const Quiz7Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 7/20',
|
||||
question: 'Qual alimento é mais prejudicial para os dentes?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Balas e chicletes pegajosos',
|
||||
description:
|
||||
'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Maçã e cenoura',
|
||||
description:
|
||||
'Frutas e vegetais crus ajudam a limpar os dentes naturalmente e são saudáveis.',
|
||||
weight: 5,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz8Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 8: Primeira Visita ao Dentista
|
||||
class Quiz8Screen extends StatelessWidget {
|
||||
const Quiz8Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 8/20',
|
||||
question: 'Quando deve ser a primeira visita ao dentista?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só quando tiver todos os dentes',
|
||||
description:
|
||||
'Esperar demais pode permitir que problemas comecem sem detecção precoce.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Apenas se sentir dor',
|
||||
description:
|
||||
'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.',
|
||||
weight: 5,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz9Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 9: Chupeta e Sucção
|
||||
class Quiz9Screen extends StatelessWidget {
|
||||
const Quiz9Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 9/20',
|
||||
question: 'Até que idade é aceitável usar chupeta?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz10Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 10: Água Fluoretada
|
||||
class Quiz10Screen extends StatelessWidget {
|
||||
const Quiz10Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 10/20',
|
||||
question: 'O flúor na água de abastecimento ajuda?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz11Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 11: Escovação Noturna
|
||||
class Quiz11Screen extends StatelessWidget {
|
||||
const Quiz11Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 11/20',
|
||||
question: 'Por que a escovação noturna é tão importante?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz12Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 12: Bebidas Ácidas
|
||||
class Quiz12Screen extends StatelessWidget {
|
||||
const Quiz12Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 12/20',
|
||||
question: 'Qual bebida é mais ácida para os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz13Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 13: Dentes de Leite
|
||||
class Quiz13Screen extends StatelessWidget {
|
||||
const Quiz13Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 13/20',
|
||||
question: 'É importante cuidar dos dentes de leite?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz14Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 14: Técnica de Escovação
|
||||
class Quiz14Screen extends StatelessWidget {
|
||||
const Quiz14Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 14/20',
|
||||
question: 'Qual é a técnica correta de escovação?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Movimentos circulares suaves',
|
||||
description:
|
||||
'Movimentos circulares ou vibratórios suaves limpam sem machucar a gengiva e removem a placa eficientemente.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz15Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 15: Enxaguante Bucal
|
||||
class Quiz15Screen extends StatelessWidget {
|
||||
const Quiz15Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 15/20',
|
||||
question: 'Crianças pequenas podem usar enxaguante bucal?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Só com orientação e produtos específicos',
|
||||
description:
|
||||
'Crianças pequenas podem engolir o produto. Existem enxaguantes infantis sem álcool e com flúor adequado.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Sim, qualquer um serve',
|
||||
description:
|
||||
'Enxaguantes para adultos podem ter álcool e concentração de flúor inadequada para crianças.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Nunca, é perigoso',
|
||||
description:
|
||||
'Com produto adequado e supervisão, pode ser usado como complemento à higiene oral.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz16Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 16: Lanche Escolar
|
||||
class Quiz16Screen extends StatelessWidget {
|
||||
const Quiz16Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 16/20',
|
||||
question: 'Qual lanche escolar é melhor para os dentes?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Frutas frescas e queijo',
|
||||
description:
|
||||
'Frutas estimulam salivação e queijo neutraliza ácidos. São opções saudáveis para os dentes.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Bolachas recheadas e sucos de caixinha',
|
||||
description:
|
||||
'Açúcar e amido ficam presos nos dentes, aumentando risco de cáries.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Salgadinhos de pacote',
|
||||
description:
|
||||
'Amidos processados ficam nos dentes e se transformam em açúcar, causando cáries.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz17Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 17: Traumas Dentários
|
||||
class Quiz17Screen extends StatelessWidget {
|
||||
const Quiz17Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 17/20',
|
||||
question: 'O que fazer se um dente de leite cair por trauma?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Procurar dentista imediatamente',
|
||||
description:
|
||||
'Mesmo sendo dente de leite, é importante avaliar se houve dano nos permanentes ou nosso tecidos.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não fazer nada, nasce outro',
|
||||
description:
|
||||
'Trauma pode afetar o dente permanente que está em formação ou causar infecções.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Tentar recolocar no lugar',
|
||||
description:
|
||||
'Não se deve recolocar dente de leite avulsionado, apenas os permanentes.',
|
||||
weight: 4,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz18Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 18: Mordedura Cruzada
|
||||
class Quiz18Screen extends StatelessWidget {
|
||||
const Quiz18Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 18/20',
|
||||
question: 'O que pode causar problemas na mordida?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Chupeta e sucção de dedo prolongadas',
|
||||
description:
|
||||
'Hábitos prolongados podem causar mordida cruzada, mordida aberta e outros problemas ortodônticos.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Genética apenas',
|
||||
description:
|
||||
'Embora genética influencie, hábitos como chupeta e sucção são grandes fatores causais.',
|
||||
weight: 4,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Não tem prevenção possível',
|
||||
description:
|
||||
'Evitar hábitos prejudiciais e fazer acompanhamento odontológico previne muitos problemas.',
|
||||
weight: 5,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz19Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 19: Gengivas Sangrando
|
||||
class Quiz19Screen extends StatelessWidget {
|
||||
const Quiz19Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 19/20',
|
||||
question: 'Gengiva sangrando ao escovar significa?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Inflamação que precisa de tratamento',
|
||||
description:
|
||||
'Sangramento indica gengivite. Não deve parar de escovar, mas sim procurar tratamento e melhorar a higiene.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Normal e não precisa se preocupar',
|
||||
description:
|
||||
'Sangramento não é normal. Indica inflamação que pode evoluir para problemas mais sérios.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Está escovando com força demais',
|
||||
description:
|
||||
'Força excessiva pode machucar, mas geralmente sangramento indica inflamação gengival.',
|
||||
weight: 3,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => Quiz20Screen(currentScore: nextScore, scopeId: scopeId),
|
||||
),
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz 20: Selantes
|
||||
class Quiz20Screen extends StatelessWidget {
|
||||
const Quiz20Screen({super.key, required this.currentScore, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QuizQuestionScreen(
|
||||
title: 'Quiz 20/20',
|
||||
question: 'Para que servem os selantes dentários?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Clarear os dentes',
|
||||
description:
|
||||
'Selantes não têm função estética de clareamento, apenas protetiva 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,
|
||||
),
|
||||
],
|
||||
currentScore: currentScore,
|
||||
nextRoute: (context, nextScore) => MaterialPageRoute<void>(
|
||||
builder: (_) => QuizResultScreen(finalScore: nextScore, maxScore: 100, scopeId: scopeId),
|
||||
),
|
||||
isFinal: true,
|
||||
showBackButton: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/quiz/quiz_prefs.dart
Normal file
67
lib/quiz/quiz_prefs.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class QuizPrefs {
|
||||
static const String _kSeenQuizKey = 'seen_oral_quiz_v1';
|
||||
static const String _kLastScoreKey = 'last_oral_quiz_score_v1';
|
||||
static const String _kLastMaxScoreKey = 'last_oral_quiz_max_score_v1';
|
||||
|
||||
static String _scopeKey(String base, String? scopeId) {
|
||||
final id = (scopeId ?? '').trim();
|
||||
if (id.isEmpty) return base;
|
||||
return '${base}_$id';
|
||||
}
|
||||
|
||||
static Future<bool> hasSeenQuiz() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_kSeenQuizKey) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> markQuizSeen() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_kSeenQuizKey, true);
|
||||
}
|
||||
|
||||
static Future<void> saveLastResult({required int score, required int maxScore}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_kLastScoreKey, score);
|
||||
await prefs.setInt(_kLastMaxScoreKey, maxScore);
|
||||
}
|
||||
|
||||
static Future<void> saveLastResultForUser({required String userId, required int score, required int maxScore}) async {
|
||||
await saveLastResultForScope(scopeId: userId, score: score, maxScore: maxScore);
|
||||
}
|
||||
|
||||
static Future<void> saveLastResultForScope({required String scopeId, required int score, required int maxScore}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_scopeKey(_kLastScoreKey, scopeId), score);
|
||||
await prefs.setInt(_scopeKey(_kLastMaxScoreKey, scopeId), maxScore);
|
||||
}
|
||||
|
||||
static Future<int?> getLastScore() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_kLastScoreKey);
|
||||
}
|
||||
|
||||
static Future<int?> getLastScoreForUser(String userId) async {
|
||||
return getLastScoreForScope(userId);
|
||||
}
|
||||
|
||||
static Future<int?> getLastScoreForScope(String scopeId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_scopeKey(_kLastScoreKey, scopeId));
|
||||
}
|
||||
|
||||
static Future<int?> getLastMaxScore() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_kLastMaxScoreKey);
|
||||
}
|
||||
|
||||
static Future<int?> getLastMaxScoreForUser(String userId) async {
|
||||
return getLastMaxScoreForScope(userId);
|
||||
}
|
||||
|
||||
static Future<int?> getLastMaxScoreForScope(String scopeId) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_scopeKey(_kLastMaxScoreKey, scopeId));
|
||||
}
|
||||
}
|
||||
326
lib/quiz/quiz_question_screen.dart
Normal file
326
lib/quiz/quiz_question_screen.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
typedef QuizNextBuilder = Route<void> Function(BuildContext context, int nextScore);
|
||||
|
||||
class QuizAnswer {
|
||||
const QuizAnswer({required this.title, required this.description, required this.weight});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final int weight;
|
||||
}
|
||||
|
||||
class QuizQuestionScreen extends StatefulWidget {
|
||||
const QuizQuestionScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.question,
|
||||
required this.answers,
|
||||
required this.nextRoute,
|
||||
this.currentScore = 0,
|
||||
this.onFinished,
|
||||
this.isFinal = false,
|
||||
this.showBackButton = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String question;
|
||||
final List<QuizAnswer> answers;
|
||||
final QuizNextBuilder nextRoute;
|
||||
final int currentScore;
|
||||
final VoidCallback? onFinished;
|
||||
final bool isFinal;
|
||||
final bool showBackButton;
|
||||
|
||||
@override
|
||||
State<QuizQuestionScreen> createState() => _QuizQuestionScreenState();
|
||||
}
|
||||
|
||||
class _QuizQuestionScreenState extends State<QuizQuestionScreen> {
|
||||
int? _selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final bool canProceed = _selected != null;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.40,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
widget.question,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFF55A7),
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Escolha apenas uma opção',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 18),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
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;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: !canProceed
|
||||
? null
|
||||
: () {
|
||||
final picked = widget.answers[_selected!];
|
||||
final nextScore = widget.currentScore + picked.weight;
|
||||
|
||||
if (widget.isFinal) {
|
||||
widget.onFinished?.call();
|
||||
Navigator.of(context).popUntil((r) => r.isFirst);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(widget.nextRoute(context, nextScore));
|
||||
},
|
||||
child: Text(widget.isFinal ? 'Concluir' : 'Avançar'),
|
||||
),
|
||||
),
|
||||
if (widget.showBackButton) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
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(),
|
||||
child: const Text('Voltar'),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizAnswerTile extends StatelessWidget {
|
||||
const _QuizAnswerTile({required this.answer, required this.selected, required this.onTap});
|
||||
|
||||
final QuizAnswer answer;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@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);
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: borderColor, width: selected ? 1.4 : 1.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
answer.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 15,
|
||||
color: Color(0xFF2F9E94),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: selected ? 0.5 : 0.0,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(
|
||||
Icons.expand_more_rounded,
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
answer.description,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.72),
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
crossFadeState: selected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 220),
|
||||
firstCurve: Curves.easeIn,
|
||||
secondCurve: Curves.easeOut,
|
||||
sizeCurve: Curves.easeOutCubic,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
433
lib/quiz/quiz_random.dart
Normal file
433
lib/quiz/quiz_random.dart
Normal file
@@ -0,0 +1,433 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'quiz_question_screen.dart';
|
||||
import 'quiz_result.dart';
|
||||
|
||||
class QuizRandomScreen extends StatefulWidget {
|
||||
const QuizRandomScreen({super.key, this.currentScore = 0, this.scopeId});
|
||||
|
||||
final int currentScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
State<QuizRandomScreen> createState() => _QuizRandomScreenState();
|
||||
}
|
||||
|
||||
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?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 2,
|
||||
title: 'Quiz 2/15',
|
||||
question: 'Quando devo trocar a escova de dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 3,
|
||||
title: 'Quiz 3/15',
|
||||
question: 'Qual a quantidade ideal de pasta de dente para crianças?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 4,
|
||||
title: 'Quiz 4/15',
|
||||
question: 'Qual é o melhor horário para usar fio dental?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 7,
|
||||
title: 'Quiz 7/15',
|
||||
question: 'Qual alimento é mais prejudicial para os dentes?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Balas e chicletes pegajosos',
|
||||
description: 'Alimentos pegajosos ficam presos nos dentes por mais tempo, aumentando o risco de cáries.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Maçã e cenoura',
|
||||
description: 'Frutas e vegetais crus ajudam a limpar os dentes naturalmente e são saudáveis.',
|
||||
weight: 5,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 8,
|
||||
title: 'Quiz 8/15',
|
||||
question: 'Quando deve ser a primeira visita ao dentista?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Só quando tiver todos os dentes',
|
||||
description: 'Esperar demais pode permitir que problemas comecem sem detecção precoce.',
|
||||
weight: 5,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Apenas se sentir dor',
|
||||
description: 'Dor geralmente indica que o problema já está avançado. Prevenção é melhor que tratamento.',
|
||||
weight: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 9,
|
||||
title: 'Quiz 9/15',
|
||||
question: 'Até que idade é aceitável usar chupeta?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 10,
|
||||
title: 'Quiz 10/15',
|
||||
question: 'O flúor na água de abastecimento ajuda?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 11,
|
||||
title: 'Quiz 11/15',
|
||||
question: 'Por que a escovação noturna é tão importante?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 12,
|
||||
title: 'Quiz 12/15',
|
||||
question: 'Qual bebida é mais ácida para os dentes?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 13,
|
||||
title: 'Quiz 13/15',
|
||||
question: 'É importante cuidar dos dentes de leite?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 14,
|
||||
title: 'Quiz 14/15',
|
||||
question: 'Qual é a técnica correta de escovação?',
|
||||
answers: const [
|
||||
QuizAnswer(
|
||||
title: 'Movimentos circulares suaves',
|
||||
description: 'Movimentos circulares ou vibratórios suaves limpam sem machucar a gengiva e removem a placa eficientemente.',
|
||||
weight: 2,
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
QuizQuestion(
|
||||
id: 15,
|
||||
title: 'Quiz 15/15',
|
||||
question: 'Para que servem os selantes dentários?',
|
||||
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.',
|
||||
weight: 2,
|
||||
),
|
||||
QuizAnswer(
|
||||
title: 'Clarear os dentes',
|
||||
description: 'Selantes não têm função estética de clareamento, apenas protetiva 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
late List<QuizQuestion> _shuffledQuestions;
|
||||
int _currentQuestionIndex = 0;
|
||||
int _currentScore = 0;
|
||||
final Random _random = Random();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentScore = widget.currentScore;
|
||||
_shuffledQuestions = List.from(_allQuestions)..shuffle(_random);
|
||||
}
|
||||
|
||||
void _nextQuestion(int scoreToAdd) {
|
||||
setState(() {
|
||||
_currentScore += scoreToAdd;
|
||||
_currentQuestionIndex++;
|
||||
});
|
||||
|
||||
if (_currentQuestionIndex >= _shuffledQuestions.length) {
|
||||
// Quiz finished
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => QuizResultScreen(
|
||||
finalScore: _currentScore,
|
||||
maxScore: 75, // 15 questions * 5 max points
|
||||
scopeId: widget.scopeId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_currentQuestionIndex >= _shuffledQuestions.length) {
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
final currentQuestion = _shuffledQuestions[_currentQuestionIndex];
|
||||
final isLastQuestion = _currentQuestionIndex == _shuffledQuestions.length - 1;
|
||||
|
||||
return QuizQuestionScreen(
|
||||
title: currentQuestion.title,
|
||||
question: currentQuestion.question,
|
||||
answers: currentQuestion.answers,
|
||||
currentScore: _currentScore,
|
||||
nextRoute: (context, nextScore) {
|
||||
_nextQuestion(nextScore - _currentScore);
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuizQuestion {
|
||||
final int id;
|
||||
final String title;
|
||||
final String question;
|
||||
final List<QuizAnswer> answers;
|
||||
|
||||
QuizQuestion({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.question,
|
||||
required this.answers,
|
||||
});
|
||||
}
|
||||
208
lib/quiz/quiz_result.dart
Normal file
208
lib/quiz/quiz_result.dart
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'quiz_prefs.dart';
|
||||
|
||||
class QuizResultScreen extends StatefulWidget {
|
||||
const QuizResultScreen({super.key, required this.finalScore, required this.maxScore, this.scopeId});
|
||||
|
||||
final int finalScore;
|
||||
final int maxScore;
|
||||
final String? scopeId;
|
||||
|
||||
@override
|
||||
State<QuizResultScreen> createState() => _QuizResultScreenState();
|
||||
}
|
||||
|
||||
class _QuizResultScreenState extends State<QuizResultScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
QuizPrefs.markQuizSeen();
|
||||
final scope = (widget.scopeId ?? '').trim();
|
||||
if (scope.isNotEmpty) {
|
||||
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);
|
||||
} else {
|
||||
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}_')) {
|
||||
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((_) {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clamped = widget.finalScore.clamp(0, widget.maxScore);
|
||||
final percent = ((clamped / widget.maxScore) * 100).round();
|
||||
final progress = percent / 100.0;
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(22, 12, 22, 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => Navigator.of(context).popUntil((r) => r.isFirst),
|
||||
child: const Text(''),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'A percentagem de risco\navaliada é de:',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFF55A7),
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 220,
|
||||
height: 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 12,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.10),
|
||||
valueColor: const AlwaysStoppedAnimation(Color(0xFF2F9E94)),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'$percent%',
|
||||
style: const TextStyle(
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${clamped.toInt()}/${widget.maxScore}',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.60),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
'Conclusões:',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.75),
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Esta avaliação é apenas educativa.\nSe tiver dúvidas ou sinais de cárie/dor, procure um Dentista.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.70),
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
child: Text(
|
||||
'Descarregar relatório (em breve)',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFF55A7).withValues(alpha: 0.95),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 260,
|
||||
height: 46,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2F9E94),
|
||||
foregroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).popUntil((r) => r.isFirst),
|
||||
child: const Text('Avançar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
212
lib/screens/curiosidade_screen.dart
Normal file
212
lib/screens/curiosidade_screen.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class CuriosidadeScreen extends StatelessWidget {
|
||||
const CuriosidadeScreen({super.key});
|
||||
|
||||
static const Color _teal = Color(0xFF2F9E94);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: _teal,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'Curiosidades',
|
||||
style: TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.40,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
children: [
|
||||
_CuriosityTopicTile(
|
||||
title: 'Tema X',
|
||||
description: 'Aprenda dicas rápidas e simples para cuidar dos dentes no dia a dia.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _CuriosityTopicTile(
|
||||
title: 'Tema Y',
|
||||
description: 'Conteúdo em breve.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _CuriosityTopicTile(
|
||||
title: 'Tema Z',
|
||||
description: 'Conteúdo em breve.',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const _CuriosityTopicTile(
|
||||
title: 'Tema U',
|
||||
description: 'Conteúdo em breve.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuriosityTopicTile extends StatelessWidget {
|
||||
const _CuriosityTopicTile({required this.title, required this.description});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.82),
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
elevation: 8,
|
||||
shadowColor: Colors.black.withValues(alpha: 0.10),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
onTap: () {
|
||||
showModalBottomSheet<void>(
|
||||
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),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFF55A7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
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: Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.72),
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(
|
||||
height: 44,
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF2F9E94),
|
||||
foregroundColor: Colors.white,
|
||||
shape: const StadiumBorder(),
|
||||
textStyle: const TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: const Text('Fechar'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 18, 16, 18),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFF55A7).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.lightbulb_rounded,
|
||||
color: Color(0xFFFF55A7),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF2F9E94),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right_rounded, color: Colors.black54),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
lib/screens/hello_splash_screen.dart
Normal file
92
lib/screens/hello_splash_screen.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HelloSplashScreen extends StatefulWidget {
|
||||
const HelloSplashScreen({super.key, required this.onFinished, this.duration = const Duration(seconds: 5)});
|
||||
|
||||
final Duration duration;
|
||||
final VoidCallback onFinished;
|
||||
|
||||
@override
|
||||
State<HelloSplashScreen> createState() => _HelloSplashScreenState();
|
||||
}
|
||||
|
||||
class _HelloSplashScreenState extends State<HelloSplashScreen> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _opacity;
|
||||
|
||||
Timer? _fadeTimer;
|
||||
Timer? _doneTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
|
||||
_opacity = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
|
||||
_controller.value = 1.0;
|
||||
|
||||
final int fadeMs = (widget.duration.inMilliseconds - 500).clamp(0, widget.duration.inMilliseconds);
|
||||
_fadeTimer = Timer(Duration(milliseconds: fadeMs), () {
|
||||
if (!mounted) return;
|
||||
_controller.reverse();
|
||||
});
|
||||
|
||||
_doneTimer = Timer(widget.duration, () {
|
||||
if (!mounted) return;
|
||||
widget.onFinished();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fadeTimer?.cancel();
|
||||
_doneTimer?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
|
||||
return Scaffold(
|
||||
body: FadeTransition(
|
||||
opacity: _opacity,
|
||||
child: Container(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
color: const Color(0xFFFFC9DF),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Text(
|
||||
'Olá',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 64,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
315
lib/screens/video_screen.dart
Normal file
315
lib/screens/video_screen.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
class VideoItem {
|
||||
const VideoItem({required this.title, required this.url});
|
||||
|
||||
final String title;
|
||||
final String url;
|
||||
}
|
||||
|
||||
class VideoScreen extends StatelessWidget {
|
||||
const VideoScreen({super.key});
|
||||
|
||||
static const Color _teal = Color(0xFF2F9E94);
|
||||
static const Color _accentPink = Color(0xFFFF55A7);
|
||||
|
||||
static const List<VideoItem> library = [
|
||||
VideoItem(title: 'Como escovar da maneira certa', url: 'https://www.youtube.com/watch?v=uH8dBWkD__0'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: _teal,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
|
||||
title: const Text(
|
||||
'Vídeos Educativos',
|
||||
style: TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFFFE6F1),
|
||||
Color(0xFFFFC9DF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -size.width * 0.40,
|
||||
bottom: -size.width * 0.45,
|
||||
child: IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: size.width * 1.05,
|
||||
height: size.width * 1.05,
|
||||
child: Transform.rotate(
|
||||
angle: 35 * math.pi / 180,
|
||||
child: Opacity(
|
||||
opacity: 0.95,
|
||||
child: Lottie.asset(
|
||||
'lottie/Liquid waves.json',
|
||||
fit: BoxFit.cover,
|
||||
repeat: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.65),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.play_circle_fill_rounded, color: _accentPink),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Vídeos Educativos',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: _accentPink,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.92,
|
||||
children: library.map((item) {
|
||||
final videoId = YoutubePlayer.convertUrlToId(item.url);
|
||||
final thumb = videoId == null ? null : 'https://img.youtube.com/vi/$videoId/0.jpg';
|
||||
return _VideoCard(
|
||||
title: item.title,
|
||||
thumbnailUrl: thumb,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => VideoPlayerScreen(url: item.url, title: item.title),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoCard extends StatelessWidget {
|
||||
const _VideoCard({required this.title, required this.thumbnailUrl, required this.onTap});
|
||||
|
||||
final String title;
|
||||
final String? thumbnailUrl;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.white.withValues(alpha: 0.80),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (thumbnailUrl != null)
|
||||
Image.network(
|
||||
thumbnailUrl!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
else
|
||||
Container(color: Colors.black.withValues(alpha: 0.06)),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0.10),
|
||||
Colors.black.withValues(alpha: 0.42),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Align(
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.play_circle_fill_rounded, size: 54, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 12),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFF2F9E94),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPlayerScreen extends StatefulWidget {
|
||||
const VideoPlayerScreen({super.key, required this.url, required this.title});
|
||||
|
||||
final String url;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<VideoPlayerScreen> createState() => _VideoPlayerScreenState();
|
||||
}
|
||||
|
||||
class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
static const Color _teal = Color(0xFF2F9E94);
|
||||
static const Color _bg = Color(0xFFFFE6F1);
|
||||
|
||||
late final YoutubePlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final id = YoutubePlayer.convertUrlToId(widget.url);
|
||||
_controller = YoutubePlayerController(
|
||||
initialVideoId: id ?? '',
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: true,
|
||||
mute: false,
|
||||
enableCaption: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasVideo = _controller.initialVideoId.isNotEmpty;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: _teal,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
color: _bg,
|
||||
child: SafeArea(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: hasVideo
|
||||
? YoutubePlayer(
|
||||
controller: _controller,
|
||||
showVideoProgressIndicator: true,
|
||||
progressIndicatorColor: Colors.white,
|
||||
)
|
||||
: Container(
|
||||
color: Colors.black.withValues(alpha: 0.10),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Link de vídeo inválido',
|
||||
style: TextStyle(fontWeight: FontWeight.w800),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color(0xFFFF55A7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Assista ao vídeo e aprenda mais sobre saúde bucal.',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.70),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user