diff --git a/assets/playmaker-logo.png b/assets/playmaker-logo.png new file mode 100644 index 0000000..d84412a Binary files /dev/null and b/assets/playmaker-logo.png differ diff --git a/flutter.create b/flutter.create deleted file mode 100644 index e69de29..0000000 diff --git a/lib/controllers/login_controller.dart b/lib/controllers/login_controller.dart new file mode 100644 index 0000000..4ca1850 --- /dev/null +++ b/lib/controllers/login_controller.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +class LoginController with ChangeNotifier { + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + + bool _isLoading = false; + bool _obscurePassword = true; + String? _emailError; + String? _passwordError; + + bool get isLoading => _isLoading; + bool get obscurePassword => _obscurePassword; + String? get emailError => _emailError; + String? get passwordError => _passwordError; + + void togglePasswordVisibility() { + _obscurePassword = !_obscurePassword; + notifyListeners(); + } + + String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Por favor, insira o seu email'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value)) { + return 'Por favor, insira um email válido'; + } + return null; + } + + String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Por favor, insira a sua password'; + } + if (value.length < 6) { + return 'A password deve ter pelo menos 6 caracteres'; + } + return null; + } + + Future login() async { + _emailError = validateEmail(emailController.text); + _passwordError = validatePassword(passwordController.text); + + if (_emailError != null || _passwordError != null) { + notifyListeners(); + return false; + } + + _isLoading = true; + notifyListeners(); + + try { + await Future.delayed(const Duration(seconds: 2)); + + // Simula login bem-sucedido + _isLoading = false; + notifyListeners(); + return true; + } catch (e) { + _isLoading = false; + _emailError = 'Erro no login. Tente novamente.'; + notifyListeners(); + return false; + } + } + + void dispose() { + emailController.dispose(); + passwordController.dispose(); + } +} \ No newline at end of file diff --git a/lib/login.dart b/lib/login.dart new file mode 100644 index 0000000..bc7903a --- /dev/null +++ b/lib/login.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'widgets/login_widgets.dart'; +import '../Controllers/login_controller.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final LoginController controller = LoginController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final screenWidth = constraints.maxWidth; + final screenHeight = constraints.maxHeight; + + return Center( + child: Container( + width: screenWidth > 800 ? 600.0 : + screenWidth > 600 ? 500.0 : 400.0, + height: screenHeight, // ← USA A ALTURA TOTAL + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, // ← CENTRALIZA VERTICALMENTE + children: [ + const Expanded( // ← EXPANDE PARA USAR ESPAÇO + flex: 2, + child: SizedBox(), + ), + + const BasketTrackHeader(), + const SizedBox(height: 40), + + LoginFormFields(controller: controller), + const SizedBox(height: 24), + + LoginButton( + controller: controller, + onLoginSuccess: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Login bem-sucedido!'), + backgroundColor: Colors.green, + ), + ); + }, + ), + const SizedBox(height: 16), + + const CreateAccountButton(), + + const Expanded( // ← EXPANDE PARA USAR ESPAÇO + flex: 3, + child: SizedBox(), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7b7f5b6..80b4aa6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'login.dart'; void main() { runApp(const MyApp()); @@ -7,116 +8,18 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'BasketTrack', theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - - @override - Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - // - // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint" - // action in the IDE, or press "p" in the console), to see the - // wireframe for each widget. - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('You have pushed the button this many times:'), - Text( - '$_counter', - style: Theme.of(context).textTheme.headlineMedium, - ), - ], + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFFE74C3C), ), + useMaterial3: true, ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: const Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. + home: const LoginPage(), + debugShowCheckedModeBanner: false, ); } -} +} \ No newline at end of file diff --git a/lib/widgets/login_widgets.dart b/lib/widgets/login_widgets.dart new file mode 100644 index 0000000..1ce35b0 --- /dev/null +++ b/lib/widgets/login_widgets.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; +import 'package:playmaker/Controllers/login_controller.dart'; + +class BasketTrackHeader extends StatelessWidget { + const BasketTrackHeader({super.key}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + // TAMANHOS AUMENTADOS para tablets + final logoSize = screenWidth > 600 ? 400.0 : 300.0; // ↑ Aumentado + final titleFontSize = screenWidth > 600 ? 48.0 : 36.0; // ↑ Aumentado + final subtitleFontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado + + return Column( + children: [ + Container( + width: logoSize, + height: logoSize, + child: Image.asset( + 'assets/playmaker-logo.png', + fit: BoxFit.contain, + ), + ), + SizedBox(height: screenWidth > 600 ? 1.0 : 1.0), + + Text( + 'BasketTrack', + style: TextStyle( + fontSize: titleFontSize, + fontWeight: FontWeight.bold, + color: Colors.grey[900], + ), + ), + SizedBox(height: screenWidth > 600 ? 1.0 : 1.0), + + Text( + 'Gere as tuas equipas e estatísticas', + style: TextStyle( + fontSize: subtitleFontSize, + color: Colors.grey[600], + fontWeight: FontWeight.w500, // ↑ Adicionado peso da fonte + ), + textAlign: TextAlign.center, + ), + ], + ); + } +} + +class LoginFormFields extends StatelessWidget { + final LoginController controller; + + const LoginFormFields({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + // TAMANHOS AUMENTADOS + final verticalPadding = screenWidth > 600 ? 26.0 : 20.0; // ↑ Aumentado + final spacing = screenWidth > 600 ? 28.0 : 20.0; // ↑ Aumentado + final labelFontSize = screenWidth > 600 ? 18.0 : 16.0; // ↑ Aumentado + final textFontSize = screenWidth > 600 ? 18.0 : 16.0; // ↑ Aumentado + + return Column( + children: [ + TextField( + controller: controller.emailController, + style: TextStyle(fontSize: textFontSize), // ↑ Tamanho do texto + decoration: InputDecoration( + labelText: 'E-mail', + labelStyle: TextStyle(fontSize: labelFontSize), // ↑ Tamanho do label + prefixIcon: Icon(Icons.email_outlined, size: 24), // ↑ Ícone maior + errorText: controller.emailError, + errorStyle: TextStyle(fontSize: 14), // ↑ Tamanho do erro + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[400]!), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[400]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE74C3C), width: 2), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 18, // ↑ Aumentado + vertical: verticalPadding, + ), + ), + keyboardType: TextInputType.emailAddress, + onChanged: (_) { + if (controller.emailError != null) { + controller.validateEmail(controller.emailController.text); + } + }, + ), + SizedBox(height: spacing), + + TextField( + controller: controller.passwordController, + style: TextStyle(fontSize: textFontSize), // ↑ Tamanho do texto + decoration: InputDecoration( + labelText: 'Palavra-passe', + labelStyle: TextStyle(fontSize: labelFontSize), // ↑ Tamanho do label + prefixIcon: Icon(Icons.lock_outlined, size: 24), // ↑ Ícone maior + suffixIcon: IconButton( + icon: Icon( + controller.obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: Colors.grey[600], + size: 24, // ↑ Ícone maior + ), + onPressed: controller.togglePasswordVisibility, + ), + errorText: controller.passwordError, + errorStyle: TextStyle(fontSize: 14), // ↑ Tamanho do erro + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[400]!), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey[400]!), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide(color: Color(0xFFE74C3C), width: 2), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 18, // ↑ Aumentado + vertical: verticalPadding, + ), + ), + obscureText: controller.obscurePassword, + onChanged: (_) { + if (controller.passwordError != null) { + controller.validatePassword(controller.passwordController.text); + } + }, + ), + SizedBox(height: screenWidth > 600 ? 20.0 : 14.0), // ↑ Aumentado + + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () {}, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), // ↑ Mais espaço + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + 'Recuperar Palavra-passe', + style: TextStyle( + fontSize: screenWidth > 600 ? 18.0 : 15.0, // ↑ Aumentado + color: const Color(0xFFE74C3C), + fontWeight: FontWeight.w600, // ↑ Mais negrito + ), + ), + ), + ), + ], + ); + } +} + +class LoginButton extends StatelessWidget { + final LoginController controller; + final VoidCallback onLoginSuccess; + + const LoginButton({ + super.key, + required this.controller, + required this.onLoginSuccess, + }); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + // BOTÕES MAIORES + final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; // ↑ Aumentado + final fontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado + + return SizedBox( + width: double.infinity, + height: buttonHeight, + child: ElevatedButton( + onPressed: controller.isLoading ? null : () async { + final success = await controller.login(); + if (success) { + onLoginSuccess(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE74C3C), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas + ), + elevation: 3, // ↑ Sombra mais pronunciada + ), + child: controller.isLoading + ? SizedBox( + width: 28, // ↑ Aumentado + height: 28, // ↑ Aumentado + child: CircularProgressIndicator( + strokeWidth: 3, // ↑ Aumentado + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Text( + 'Entrar', + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w700, // ↑ Mais negrito + ), + ), + ), + ); + } +} + +class CreateAccountButton extends StatelessWidget { + const CreateAccountButton({super.key}); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + // BOTÃO MAIOR + final buttonHeight = screenWidth > 600 ? 70.0 : 58.0; // ↑ Aumentado + final fontSize = screenWidth > 600 ? 22.0 : 18.0; // ↑ Aumentado + + return SizedBox( + width: double.infinity, + height: buttonHeight, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFE74C3C), + side: const BorderSide(color: Color(0xFFE74C3C), width: 2), // ↑ Borda mais grossa + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), // ↑ Bordas mais arredondadas + ), + ), + child: Text( + 'Criar Conta', + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w700, // ↑ Mais negrito + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c1b51d3..efd3023 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -131,6 +131,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -139,6 +147,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 592bd50..3a843f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + provider: ^6.1.5+1 dev_dependencies: flutter_test: @@ -51,16 +52,10 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + + assets: + - assets/playmaker-logo.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images