Tela de login completa

main
Diogo 2025-11-27 22:17:05 +00:00
parent 91b1a21e04
commit bf2bd41eb2
8 changed files with 443 additions and 114 deletions

BIN
assets/playmaker-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

View File

@ -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<bool> 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();
}
}

79
lib/login.dart Normal file
View File

@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
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(),
),
],
),
),
);
},
),
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'login.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@ -7,116 +8,18 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'BasketTrack',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application. colorScheme: ColorScheme.fromSeed(
// seedColor: const Color(0xFFE74C3C),
// 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'), useMaterial3: true,
); ),
} home: const LoginPage(),
} debugShowCheckedModeBanner: false,
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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
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: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

View File

@ -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<Color>(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
),
),
),
);
}
}

View File

@ -131,6 +131,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -139,6 +147,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -34,6 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
provider: ^6.1.5+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -51,16 +52,10 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: 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 uses-material-design: true
# To add assets to your application, add an assets section, like this: assets:
# assets: - assets/playmaker-logo.png
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images