From 44f5703de4db71d0f52f7a16a4d5738638a59969 Mon Sep 17 00:00:00 2001 From: Patryk Hegenberg Date: Sun, 4 Jan 2026 19:13:59 +0100 Subject: [PATCH] feat: improve ui --- .../presentation/screens/login_screen.dart | 362 ++++++++++------ .../presentation/screens/register_screen.dart | 388 ++++++++++++------ pubspec.lock | 8 + pubspec.yaml | 8 +- 4 files changed, 513 insertions(+), 253 deletions(-) diff --git a/lib/src/features/authentication/presentation/screens/login_screen.dart b/lib/src/features/authentication/presentation/screens/login_screen.dart index 3997300..b822864 100644 --- a/lib/src/features/authentication/presentation/screens/login_screen.dart +++ b/lib/src/features/authentication/presentation/screens/login_screen.dart @@ -16,18 +16,30 @@ class _LoginScreenState extends ConsumerState { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); + final _emailFocusNode = FocusNode(); + final _passwordFocusNode = FocusNode(); + bool _isLoading = false; bool _obscurePassword = true; + String? _errorMessage; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); + _emailFocusNode.dispose(); + _passwordFocusNode.dispose(); super.dispose(); } Future _handleLogin() async { - if (!_formKey.currentState!.validate()) return; + FocusScope.of(context).unfocus(); + + setState(() => _errorMessage = null); + + if (!_formKey.currentState!.validate()) { + return; + } setState(() => _isLoading = true); @@ -43,147 +55,235 @@ class _LoginScreenState extends ConsumerState { } } catch (e) { if (mounted) { - String message = 'Login failed. Please try again.'; - final errorStr = e.toString(); - - if (errorStr.contains('400')) { - message = 'Invalid email or password.'; - } else if (errorStr.contains('SocketException') || - errorStr.contains('Connection refused') || - errorStr.contains('Network is unreachable')) { - message = 'Could not connect to server. Please check your internet.'; - } - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: AppTheme.errorColor, - behavior: SnackBarBehavior.floating, - ), - ); - } - } finally { - if (mounted) { - setState(() => _isLoading = false); + setState(() { + _isLoading = false; + _errorMessage = _parseErrorMessage(e.toString()); + }); } } } + String _parseErrorMessage(String error) { + if (error.contains('400')) { + return 'Invalid email or password'; + } else if (error.contains('SocketException') || + error.contains('Connection refused') || + error.contains('Network is unreachable')) { + return 'Could not connect to server.\nPlease check your internet connection.'; + } else if (error.contains('timeout')) { + return 'Connection timeout.\nPlease try again.'; + } + return 'Login failed. Please try again.'; + } + @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: AppTheme.primaryColor, - borderRadius: BorderRadius.circular(20), - ), - child: const Icon( - Icons.fitness_center, - size: 56, - color: Colors.black, - ), + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, ), - const SizedBox(height: 24), - Text( - 'WELCOME BACK', - style: Theme.of(context).textTheme.displayMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 8), - Text( - 'Time to level up your strength', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 48), - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: 'Password', - prefixIcon: const Icon(Icons.lock_outline), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility_outlined - : Icons.visibility_off_outlined, - ), - onPressed: () { - setState(() => _obscurePassword = !_obscurePassword); - }, - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password'; - } - if (value.length < 8) { - return 'Password must be at least 8 characters'; - } - return null; - }, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: _isLoading ? null : _handleLogin, - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.black, + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Spacer(), + + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.primaryColor + .withValues(alpha: 0.4), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: const Icon( + Icons.fitness_center, + size: 56, + color: Colors.black, + ), ), - ) - : const Text('LOGIN'), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Don't have an account? ", - style: Theme.of(context).textTheme.bodyMedium, + const SizedBox(height: 32), + + Text( + 'WELCOME BACK', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Time to level up your strength', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + + if (_errorMessage != null) + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: AppTheme.errorColor + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.errorColor, + width: 1, + ), + ), + child: Row( + children: [ + const Icon( + Icons.error_outline, + color: AppTheme.errorColor, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _errorMessage!, + style: const TextStyle( + color: AppTheme.errorColor, + fontSize: 14, + ), + ), + ), + ], + ), + ), + + TextFormField( + controller: _emailController, + focusNode: _emailFocusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + enabled: !_isLoading, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email_outlined), + ), + onFieldSubmitted: (_) { + _passwordFocusNode.requestFocus(); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Password Field + TextFormField( + controller: _passwordController, + focusNode: _passwordFocusNode, + obscureText: _obscurePassword, + textInputAction: TextInputAction.done, + enabled: !_isLoading, + decoration: InputDecoration( + labelText: 'Password', + prefixIcon: const Icon(Icons.lock_outline), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + onFieldSubmitted: (_) => _handleLogin(), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your password'; + } + if (value.length < 8) { + return 'Password must be at least 8 characters'; + } + return null; + }, + ), + const SizedBox(height: 32), + + ElevatedButton( + onPressed: _isLoading ? null : _handleLogin, + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 16), + disabledBackgroundColor: AppTheme.primaryColor + .withValues(alpha: 0.5), + ), + child: _isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.black, + ), + ) + : const Text( + 'LOGIN', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ), + const SizedBox(height: 16), + + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Don't have an account? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: _isLoading + ? null + : () => context.go('/register'), + child: const Text('REGISTER'), + ), + ], + ), + + const Spacer(), + ], + ), ), - TextButton( - onPressed: () => context.go('/register'), - child: const Text('REGISTER'), - ), - ], + ), ), - ], - ), - ), + ), + ); + }, ), ), ), diff --git a/lib/src/features/authentication/presentation/screens/register_screen.dart b/lib/src/features/authentication/presentation/screens/register_screen.dart index cb46ff7..0f29c27 100644 --- a/lib/src/features/authentication/presentation/screens/register_screen.dart +++ b/lib/src/features/authentication/presentation/screens/register_screen.dart @@ -1,9 +1,180 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:go_router/go_router.dart'; + +// import '../../../../core/theme/app_theme.dart'; +// import '../../../../core/constants/app_constants.dart'; +// import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart'; + +// class RegisterScreen extends ConsumerStatefulWidget { +// const RegisterScreen({super.key}); + +// @override +// ConsumerState createState() => _RegisterScreenState(); +// } + +// class _RegisterScreenState extends ConsumerState { +// final _formKey = GlobalKey(); +// final _emailController = TextEditingController(); +// // final _passwordController = TextEditingController(); +// // final _confirmPasswordController = TextEditingController(); +// // bool _obscurePassword = true; +// // bool _obscureConfirmPassword = true; + +// @override +// void dispose() { +// _emailController.dispose(); +// // _passwordController.dispose(); +// // _confirmPasswordController.dispose(); +// super.dispose(); +// } + +// void _handleRegister() { +// if (!_formKey.currentState!.validate()) return; + +// ref.read(onboardingDataProvider.notifier).updateData({ +// 'email': _emailController.text.trim(), +// // 'password': _passwordController.text, +// }); + +// context.go('/onboarding/welcome'); +// } + +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// leading: IconButton( +// icon: const Icon(Icons.arrow_back), +// onPressed: () => context.go('/login'), +// ), +// ), +// body: SafeArea( +// child: Center( +// child: SingleChildScrollView( +// padding: const EdgeInsets.all(24), +// child: Form( +// key: _formKey, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// Text( +// 'CREATE ACCOUNT', +// style: Theme.of(context).textTheme.displayMedium, +// textAlign: TextAlign.center, +// ), +// const SizedBox(height: 8), +// Text( +// 'Begin your strength journey', +// style: Theme.of(context).textTheme.bodyMedium, +// textAlign: TextAlign.center, +// ), +// const SizedBox(height: 48), +// TextFormField( +// controller: _emailController, +// keyboardType: TextInputType.emailAddress, +// decoration: const InputDecoration( +// labelText: 'Email', +// prefixIcon: Icon(Icons.email_outlined), +// ), +// validator: (value) { +// if (value == null || value.isEmpty) { +// return 'Please enter your email'; +// } +// if (!value.contains('@')) { +// return 'Please enter a valid email'; +// } +// return null; +// }, +// ), +// // const SizedBox(height: 16), +// // TextFormField( +// // controller: _passwordController, +// // obscureText: _obscurePassword, +// // decoration: InputDecoration( +// // labelText: 'Password', +// // prefixIcon: const Icon(Icons.lock_outline), +// // suffixIcon: IconButton( +// // icon: Icon( +// // _obscurePassword +// // ? Icons.visibility_outlined +// // : Icons.visibility_off_outlined, +// // ), +// // onPressed: () { +// // setState(() => _obscurePassword = !_obscurePassword); +// // }, +// // ), +// // ), +// // validator: (value) { +// // if (value == null || value.isEmpty) { +// // return 'Please enter a password'; +// // } +// // if (value.length < 8) { +// // return 'Password must be at least 8 characters'; +// // } +// // return null; +// // }, +// // ), +// // const SizedBox(height: 16), +// // TextFormField( +// // controller: _confirmPasswordController, +// // obscureText: _obscureConfirmPassword, +// // decoration: InputDecoration( +// // labelText: 'Confirm Password', +// // prefixIcon: const Icon(Icons.lock_outline), +// // suffixIcon: IconButton( +// // icon: Icon( +// // _obscureConfirmPassword +// // ? Icons.visibility_outlined +// // : Icons.visibility_off_outlined, +// // ), +// // onPressed: () { +// // setState(() => _obscureConfirmPassword = +// // !_obscureConfirmPassword); +// // }, +// // ), +// // ), +// // validator: (value) { +// // if (value != _passwordController.text) { +// // return 'Passwords do not match'; +// // } +// // return null; +// // }, +// // ), +// const SizedBox(height: 32), +// ElevatedButton( +// onPressed: _handleRegister, +// child: const Text('CONTINUE'), +// ), +// const SizedBox(height: 16), +// Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Text( +// 'Already have an account? ', +// style: Theme.of(context).textTheme.bodyMedium, +// ), +// TextButton( +// onPressed: () => context.go('/login'), +// child: const Text('LOGIN'), +// ), +// ], +// ), +// ], +// ), +// ), +// ), +// ), +// ), +// ); +// } +// } + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/theme/app_theme.dart'; -import '../../../../core/constants/app_constants.dart'; import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart'; class RegisterScreen extends ConsumerStatefulWidget { @@ -16,25 +187,24 @@ class RegisterScreen extends ConsumerStatefulWidget { class _RegisterScreenState extends ConsumerState { final _formKey = GlobalKey(); final _emailController = TextEditingController(); - // final _passwordController = TextEditingController(); - // final _confirmPasswordController = TextEditingController(); - // bool _obscurePassword = true; - // bool _obscureConfirmPassword = true; + final _emailFocusNode = FocusNode(); @override void dispose() { _emailController.dispose(); - // _passwordController.dispose(); - // _confirmPasswordController.dispose(); + _emailFocusNode.dispose(); super.dispose(); } void _handleRegister() { - if (!_formKey.currentState!.validate()) return; + FocusScope.of(context).unfocus(); + + if (!_formKey.currentState!.validate()) { + return; + } ref.read(onboardingDataProvider.notifier).updateData({ 'email': _emailController.text.trim(), - // 'password': _passwordController.text, }); context.go('/onboarding/welcome'); @@ -49,120 +219,98 @@ class _RegisterScreenState extends ConsumerState { onPressed: () => context.go('/login'), ), ), - body: SafeArea( - child: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'CREATE ACCOUNT', - style: Theme.of(context).textTheme.displayMedium, - textAlign: TextAlign.center, + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, ), - const SizedBox(height: 8), - Text( - 'Begin your strength journey', - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 48), - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: const InputDecoration( - labelText: 'Email', - prefixIcon: Icon(Icons.email_outlined), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Spacer(), + Text( + 'CREATE ACCOUNT', + style: Theme.of(context).textTheme.displayMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Begin your strength journey', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 48), + TextFormField( + controller: _emailController, + focusNode: _emailFocusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + labelText: 'Email', + prefixIcon: Icon(Icons.email_outlined), + helperText: 'You will use this to login', + ), + onFieldSubmitted: (_) {}, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: _handleRegister, + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text( + 'CONTINUE', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Already have an account? ', + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: () => context.go('/login'), + child: const Text('LOGIN'), + ), + ], + ), + const Spacer(), + ], + ), + ), ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email'; - } - if (!value.contains('@')) { - return 'Please enter a valid email'; - } - return null; - }, ), - // const SizedBox(height: 16), - // TextFormField( - // controller: _passwordController, - // obscureText: _obscurePassword, - // decoration: InputDecoration( - // labelText: 'Password', - // prefixIcon: const Icon(Icons.lock_outline), - // suffixIcon: IconButton( - // icon: Icon( - // _obscurePassword - // ? Icons.visibility_outlined - // : Icons.visibility_off_outlined, - // ), - // onPressed: () { - // setState(() => _obscurePassword = !_obscurePassword); - // }, - // ), - // ), - // validator: (value) { - // if (value == null || value.isEmpty) { - // return 'Please enter a password'; - // } - // if (value.length < 8) { - // return 'Password must be at least 8 characters'; - // } - // return null; - // }, - // ), - // const SizedBox(height: 16), - // TextFormField( - // controller: _confirmPasswordController, - // obscureText: _obscureConfirmPassword, - // decoration: InputDecoration( - // labelText: 'Confirm Password', - // prefixIcon: const Icon(Icons.lock_outline), - // suffixIcon: IconButton( - // icon: Icon( - // _obscureConfirmPassword - // ? Icons.visibility_outlined - // : Icons.visibility_off_outlined, - // ), - // onPressed: () { - // setState(() => _obscureConfirmPassword = - // !_obscureConfirmPassword); - // }, - // ), - // ), - // validator: (value) { - // if (value != _passwordController.text) { - // return 'Passwords do not match'; - // } - // return null; - // }, - // ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: _handleRegister, - child: const Text('CONTINUE'), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Already have an account? ', - style: Theme.of(context).textTheme.bodyMedium, - ), - TextButton( - onPressed: () => context.go('/login'), - child: const Text('LOGIN'), - ), - ], - ), - ], - ), - ), + ), + ); + }, ), ), ), diff --git a/pubspec.lock b/pubspec.lock index bdf41ac..c5e912b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -382,6 +382,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index f20c182..8fc9ea7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,15 +1,16 @@ name: slrpg_app description: Streetlifting RPG - Gamified Training App -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 environment: - sdk: '>=3.2.0 <4.0.0' + sdk: ">=3.2.0 <4.0.0" dependencies: flutter: sdk: flutter audioplayers: ^6.0.0 + flutter_dotenv: ^5.1.0 # State Management flutter_riverpod: ^3.1.0 @@ -69,6 +70,9 @@ flutter: - assets/images/enemies/ - assets/images/backgrounds/ - assets/audio/ + - .env + - .env.development + - .env.production # fonts: # - family: PixelFont