diff --git a/lib/core/consts/routes_const.dart b/lib/core/consts/routes_const.dart new file mode 100644 index 0000000..67c860e --- /dev/null +++ b/lib/core/consts/routes_const.dart @@ -0,0 +1,6 @@ +class AppNameRoutes { + static const String home = '/'; + static const String login = '/login'; + static const String onboarding = '/onboarding'; + static const String register = '/register'; +} diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 45cb6a4..0680c21 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -1,34 +1,39 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:quentinha_app/core/consts/routes_const.dart'; +import 'package:quentinha_app/presentation/view/register_page.dart'; import '../../presentation/view/home_page.dart'; import '../../presentation/view/login_page.dart'; import '../../presentation/view/onboarding_page.dart'; class AppRoutes { - static const String home = '/'; - static const String login = '/login'; - static const String onboarding = '/onboarding'; + static GoRouter router(bool seenOnboarding) { return GoRouter( - initialLocation: seenOnboarding ? home : onboarding, + initialLocation: seenOnboarding ? AppNameRoutes.home : AppNameRoutes.onboarding, routes: [ GoRoute( - path: home, + path: AppNameRoutes.home, name: 'home', builder: (context, state) => const HomePage(), ), GoRoute( - path: login, + path: AppNameRoutes.login, name: 'login', builder: (context, state) => const LoginPage(), ), GoRoute( - path: onboarding, + path: AppNameRoutes.onboarding, name: 'onboarding', builder: (context, state) => const OnboardingPage(), ), + GoRoute( + path: AppNameRoutes.register, + name: 'register', + builder: (context, state) => const RegisterPage(), + ), ], errorBuilder: (context, state) => Scaffold( body: Center(child: Text('Erro: ${state.error}')), diff --git a/lib/main.dart b/lib/main.dart index 28133d1..1d93082 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:quentinha_app/core/consts/colors_const.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'core/routes/routes.dart'; @@ -26,8 +27,11 @@ class MyApp extends StatelessWidget { selectionColor: Colors.orangeAccent, // texto selecionado selectionHandleColor: Colors.orange, // "bolinha" do seletor ), + textTheme: GoogleFonts.robotoTextTheme( + Theme.of(context).textTheme, + ), ), - title: 'Onboarding Demo', + title: 'Quentinhas App', debugShowCheckedModeBanner: false, routerConfig: AppRoutes.router(seenOnboarding), ); diff --git a/lib/presentation/components/password_field_widget.dart b/lib/presentation/components/password_field_widget.dart new file mode 100644 index 0000000..4fc17db --- /dev/null +++ b/lib/presentation/components/password_field_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class PasswordField extends StatefulWidget { + final TextEditingController controller; + final String? Function(String?)? validator; + + const PasswordField({ + super.key, + required this.controller, + this.validator, + }); + + @override + State createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _obscureText = true; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: widget.controller, + obscureText: _obscureText, + validator: widget.validator, + decoration: InputDecoration( + hintText: '••••••••', + filled: true, + fillColor: Colors.grey[300], + suffixIcon: IconButton( + icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility), + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + ); + } +} diff --git a/lib/presentation/components/register_login_page.dart b/lib/presentation/components/register_login_page.dart index 34175bc..993acef 100644 --- a/lib/presentation/components/register_login_page.dart +++ b/lib/presentation/components/register_login_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:quentinha_app/core/consts/colors_const.dart'; +import 'package:quentinha_app/core/consts/routes_const.dart'; import 'package:quentinha_app/core/consts/size_const.dart'; import '../../core/log/logger.dart'; @@ -104,7 +105,7 @@ class RegisterLoginPage extends StatelessWidget { // Texto rodapé TextButton( onPressed: () { - context.go('/login'); + context.go(AppNameRoutes.login); AppLogger.i("Navegar para login"); }, child: const Text( diff --git a/lib/presentation/view/login_page.dart b/lib/presentation/view/login_page.dart index ca89181..486d5d7 100644 --- a/lib/presentation/view/login_page.dart +++ b/lib/presentation/view/login_page.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:quentinha_app/core/consts/colors_const.dart'; import 'package:quentinha_app/core/consts/size_const.dart'; +import 'package:quentinha_app/presentation/components/password_field_widget.dart'; + +import '../../core/consts/routes_const.dart'; +import '../../core/log/logger.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -11,7 +16,20 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _passwordController = TextEditingController(); bool rememberAccount = false; + + void _signOn() { + if (_formKey.currentState!.validate()) { + final senha = _passwordController.text; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Cadastro realizado com sucesso!")), + ); + AppLogger.i("Senha cadastrada: $senha"); + } + } + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -59,6 +77,7 @@ class _LoginPageState extends State { // Formulário Form( + key: _formKey, child: ConstrainedBox( constraints: BoxConstraints( minHeight: @@ -68,7 +87,10 @@ class _LoginPageState extends State { child: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.only(topLeft: Radius.circular(35), topRight: Radius.circular(35)), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(35), + topRight: Radius.circular(35), + ), boxShadow: [ BoxShadow( color: Colors.black12, @@ -101,21 +123,27 @@ class _LoginPageState extends State { ), ), ), - + const SizedBox(height: 20), - + // Password const Text("SENHA"), const SizedBox(height: 8), - StatefulBuilder( - builder: (context, setState) { - // Move _obscureText outside the builder to persist its state - return _PasswordField(); + PasswordField( + controller: _passwordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Digite a senha"; + } + if (value.length < 6) { + return "A senha deve ter pelo menos 6 caracteres"; + } + return null; }, ), - + const SizedBox(height: 10), - + // Remember + Forgot Password Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -124,14 +152,15 @@ class _LoginPageState extends State { children: [ Checkbox( value: rememberAccount, - activeColor: - AppColors.primary, + activeColor: AppColors.primary, onChanged: (bool? newValue) { setState(() { rememberAccount = newValue ?? false; }); - - ScaffoldMessenger.of(context).showSnackBar( + + ScaffoldMessenger.of( + context, + ).showSnackBar( SnackBar( content: Text( rememberAccount @@ -155,15 +184,15 @@ class _LoginPageState extends State { ), ], ), - + const SizedBox(height: 20), - + // Botão Login SizedBox( width: double.infinity, height: 50, child: ElevatedButton( - onPressed: () {}, + onPressed: _signOn, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, shape: RoundedRectangleBorder( @@ -171,7 +200,7 @@ class _LoginPageState extends State { ), ), child: const Text( - "LOG IN", + "ENTRAR", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -180,16 +209,18 @@ class _LoginPageState extends State { ), ), ), - + const SizedBox(height: 20), - + // Signup Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Não tem conta? "), GestureDetector( - onTap: () {}, + onTap: () { + context.push(AppNameRoutes.register); + }, child: const Text( "Inscreva-se", style: TextStyle( @@ -200,9 +231,9 @@ class _LoginPageState extends State { ), ], ), - + const SizedBox(height: 20), - + // Divider Or Row( children: const [ @@ -214,9 +245,9 @@ class _LoginPageState extends State { Expanded(child: Divider()), ], ), - + const SizedBox(height: 20), - + // Social buttons Row( mainAxisAlignment: MainAxisAlignment.center, @@ -271,41 +302,3 @@ class _LoginPageState extends State { ); } } - -// Custom password field widget to persist obscureText state -class _PasswordField extends StatefulWidget { - @override - State<_PasswordField> createState() => _PasswordFieldState(); -} - -class _PasswordFieldState extends State<_PasswordField> { - bool _obscureText = true; - - @override - Widget build(BuildContext context) { - return TextField( - obscureText: _obscureText, - decoration: InputDecoration( - hintText: "••••••••", - filled: true, - fillColor: Colors.grey[300], - suffixIcon: IconButton( - icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - ), - ); - } -} diff --git a/lib/presentation/view/onboarding_page.dart b/lib/presentation/view/onboarding_page.dart index 89f7ea7..ba186b1 100644 --- a/lib/presentation/view/onboarding_page.dart +++ b/lib/presentation/view/onboarding_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:introduction_screen/introduction_screen.dart'; import 'package:quentinha_app/core/consts/colors_const.dart'; +import 'package:quentinha_app/core/consts/routes_const.dart'; import 'package:quentinha_app/presentation/components/register_login_page.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,7 +19,7 @@ class OnboardingPage extends StatelessWidget { // Usando GoRouter para navegação // Certifique-se de ter configurado a rota '/home' no seu GoRouter // e de importar 'package:go_router/go_router.dart' - context.go('/home'); + context.go(AppNameRoutes.home); } @override diff --git a/lib/presentation/view/register_page.dart b/lib/presentation/view/register_page.dart new file mode 100644 index 0000000..875c426 --- /dev/null +++ b/lib/presentation/view/register_page.dart @@ -0,0 +1,271 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:quentinha_app/core/consts/colors_const.dart'; +import 'package:quentinha_app/core/consts/size_const.dart'; +import 'package:quentinha_app/core/log/logger.dart'; + +import '../components/password_field_widget.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + bool rememberAccount = false; + + @override + void dispose() { + _passwordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + void _register() { + if (_formKey.currentState!.validate()) { + final senha = _passwordController.text; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Cadastro realizado com sucesso!")), + ); + AppLogger.i("Senha cadastrada: $senha"); + } + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Scaffold( + backgroundColor: AppColors.primary, + body: SafeArea( + top: false, + child: SingleChildScrollView( + child: Column( + children: [ + // Cabeçalho arredondado + Container( + width: double.infinity, + height: size.height * 0.25, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(40), + bottomRight: Radius.circular(40), + ), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Registrar", + style: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 8), + Padding( + padding: EdgeInsets.all(16.0), + child: Text( + "Crie sua conta para começar a usar o app e fazer pedidos! É rápido e fácil.", + style: TextStyle(color: Colors.white70), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + + // Formulário + Form( + key: _formKey, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + MediaQuery.of(context).size.height - + (size.height * 0.25), // pega a tela menos o header + ), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(35), + topRight: Radius.circular(35), + ), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 20.h, + // Email + const Text("EMAIL"), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + hintText: "exemplo@gmail.com", + filled: true, + fillColor: Colors.grey[300], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "Digite o e-mail"; + } + if (!value.contains("@")) { + return "E-mail inválido"; + } + return null; + }, + ), + + const SizedBox(height: 20), + + // Password + const Text("SENHA"), + const SizedBox(height: 8), + PasswordField( + controller: _passwordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Digite a senha"; + } + if (value.length < 6) { + return "A senha deve ter pelo menos 6 caracteres"; + } + return null; + }, + ), + + const SizedBox(height: 20), + + // Confirm Password + const Text("CONFIRMAR SENHA"), + const SizedBox(height: 8), + PasswordField( + controller: _confirmPasswordController, + validator: (value) { + if (value == null || value.isEmpty) { + return "Confirme a senha"; + } + if (value != _passwordController.text) { + return "As senhas não coincidem"; + } + return null; + }, + ), + + const SizedBox(height: 20), + + // Botão Registrar + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _register, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + "REGISTRAR-SE", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + + const SizedBox(height: 20), + + + + // Divider Or + Row( + children: const [ + Expanded(child: Divider()), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Text("Se preferir"), + ), + Expanded(child: Divider()), + ], + ), + + const SizedBox(height: 20), + + // Social buttons + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _socialButton( + SvgPicture.network( + "https://cdn.jsdelivr.net/npm/simple-icons@v3/icons/apple.svg", + height: 24, + colorFilter: const ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), + ), + Colors.black, + 3, + ), + const SizedBox(width: 16), + _socialButton( + SvgPicture.network( + "https://cdn.jsdelivr.net/gh/devicons/devicon/icons/google/google-original.svg", + height: 24, + ), + Colors.white24, + 3, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _socialButton(dynamic icon, Color? color, double elevation) { + return Material( + elevation: elevation, + shape: const CircleBorder(), + child: CircleAvatar( + radius: 24, + backgroundColor: color, + child: icon is IconData + ? Icon(icon, color: Colors.white, size: 28) + : icon, + ), + ); + } +}