feat: improve ui

This commit is contained in:
Patryk Hegenberg 2026-01-04 19:13:59 +01:00
parent 940f73809c
commit 44f5703de4
4 changed files with 513 additions and 253 deletions

View file

@ -16,18 +16,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
final _emailFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
bool _isLoading = false; bool _isLoading = false;
bool _obscurePassword = true; bool _obscurePassword = true;
String? _errorMessage;
@override @override
void dispose() { void dispose() {
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose(); _passwordController.dispose();
_emailFocusNode.dispose();
_passwordFocusNode.dispose();
super.dispose(); super.dispose();
} }
Future<void> _handleLogin() async { Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return; FocusScope.of(context).unfocus();
setState(() => _errorMessage = null);
if (!_formKey.currentState!.validate()) {
return;
}
setState(() => _isLoading = true); setState(() => _isLoading = true);
@ -43,147 +55,235 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
String message = 'Login failed. Please try again.'; setState(() {
final errorStr = e.toString(); _isLoading = false;
_errorMessage = _parseErrorMessage(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);
} }
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: SafeArea( body: GestureDetector(
child: Center( onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView( child: SafeArea(
padding: const EdgeInsets.all(24), child: LayoutBuilder(
child: Form( builder: (context, constraints) {
key: _formKey, return SingleChildScrollView(
child: Column( physics: const ClampingScrollPhysics(),
mainAxisAlignment: MainAxisAlignment.center, child: ConstrainedBox(
crossAxisAlignment: CrossAxisAlignment.stretch, constraints: BoxConstraints(
children: [ minHeight: constraints.maxHeight,
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,
),
), ),
const SizedBox(height: 24), child: IntrinsicHeight(
Text( child: Padding(
'WELCOME BACK', padding: const EdgeInsets.all(24),
style: Theme.of(context).textTheme.displayMedium, child: Form(
textAlign: TextAlign.center, key: _formKey,
), child: Column(
const SizedBox(height: 8), mainAxisAlignment: MainAxisAlignment.center,
Text( crossAxisAlignment: CrossAxisAlignment.stretch,
'Time to level up your strength', children: [
style: Theme.of(context).textTheme.bodyMedium, const Spacer(),
textAlign: TextAlign.center,
), Container(
const SizedBox(height: 48), width: 100,
TextFormField( height: 100,
controller: _emailController, decoration: BoxDecoration(
keyboardType: TextInputType.emailAddress, color: AppTheme.primaryColor,
decoration: const InputDecoration( borderRadius: BorderRadius.circular(20),
labelText: 'Email', boxShadow: [
prefixIcon: Icon(Icons.email_outlined), BoxShadow(
), color: AppTheme.primaryColor
validator: (value) { .withValues(alpha: 0.4),
if (value == null || value.isEmpty) { blurRadius: 20,
return 'Please enter your email'; spreadRadius: 2,
} ),
if (!value.contains('@')) { ],
return 'Please enter a valid email'; ),
} child: const Icon(
return null; Icons.fitness_center,
}, size: 56,
), color: Colors.black,
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,
), ),
) const SizedBox(height: 32),
: const Text('LOGIN'),
), Text(
const SizedBox(height: 16), 'WELCOME BACK',
Row( style: Theme.of(context).textTheme.displayMedium,
mainAxisAlignment: MainAxisAlignment.center, textAlign: TextAlign.center,
children: [ ),
Text( const SizedBox(height: 8),
"Don't have an account? ", Text(
style: Theme.of(context).textTheme.bodyMedium, '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'),
),
],
), ),
], ),
), );
), },
), ),
), ),
), ),

View file

@ -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<RegisterScreen> createState() => _RegisterScreenState();
// }
// class _RegisterScreenState extends ConsumerState<RegisterScreen> {
// final _formKey = GlobalKey<FormState>();
// 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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme.dart';
import '../../../../core/constants/app_constants.dart';
import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart'; import '../../../onboarding/presentation/screens/bodyweight_input_screen.dart';
class RegisterScreen extends ConsumerStatefulWidget { class RegisterScreen extends ConsumerStatefulWidget {
@ -16,25 +187,24 @@ class RegisterScreen extends ConsumerStatefulWidget {
class _RegisterScreenState extends ConsumerState<RegisterScreen> { class _RegisterScreenState extends ConsumerState<RegisterScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
// final _passwordController = TextEditingController(); final _emailFocusNode = FocusNode();
// final _confirmPasswordController = TextEditingController();
// bool _obscurePassword = true;
// bool _obscureConfirmPassword = true;
@override @override
void dispose() { void dispose() {
_emailController.dispose(); _emailController.dispose();
// _passwordController.dispose(); _emailFocusNode.dispose();
// _confirmPasswordController.dispose();
super.dispose(); super.dispose();
} }
void _handleRegister() { void _handleRegister() {
if (!_formKey.currentState!.validate()) return; FocusScope.of(context).unfocus();
if (!_formKey.currentState!.validate()) {
return;
}
ref.read(onboardingDataProvider.notifier).updateData({ ref.read(onboardingDataProvider.notifier).updateData({
'email': _emailController.text.trim(), 'email': _emailController.text.trim(),
// 'password': _passwordController.text,
}); });
context.go('/onboarding/welcome'); context.go('/onboarding/welcome');
@ -49,120 +219,98 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
onPressed: () => context.go('/login'), onPressed: () => context.go('/login'),
), ),
), ),
body: SafeArea( body: GestureDetector(
child: Center( onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView( child: SafeArea(
padding: const EdgeInsets.all(24), child: LayoutBuilder(
child: Form( builder: (context, constraints) {
key: _formKey, return SingleChildScrollView(
child: Column( physics: const ClampingScrollPhysics(),
crossAxisAlignment: CrossAxisAlignment.stretch, child: ConstrainedBox(
children: [ constraints: BoxConstraints(
Text( minHeight: constraints.maxHeight,
'CREATE ACCOUNT',
style: Theme.of(context).textTheme.displayMedium,
textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), child: IntrinsicHeight(
Text( child: Padding(
'Begin your strength journey', padding: const EdgeInsets.all(24),
style: Theme.of(context).textTheme.bodyMedium, child: Form(
textAlign: TextAlign.center, key: _formKey,
), child: Column(
const SizedBox(height: 48), crossAxisAlignment: CrossAxisAlignment.stretch,
TextFormField( children: [
controller: _emailController, const Spacer(),
keyboardType: TextInputType.emailAddress, Text(
decoration: const InputDecoration( 'CREATE ACCOUNT',
labelText: 'Email', style: Theme.of(context).textTheme.displayMedium,
prefixIcon: Icon(Icons.email_outlined), 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'),
),
],
),
],
),
),
), ),
), ),
), ),

View file

@ -382,6 +382,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:

View file

@ -1,15 +1,16 @@
name: slrpg_app name: slrpg_app
description: Streetlifting RPG - Gamified Training App description: Streetlifting RPG - Gamified Training App
publish_to: 'none' publish_to: "none"
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=3.2.0 <4.0.0' sdk: ">=3.2.0 <4.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
audioplayers: ^6.0.0 audioplayers: ^6.0.0
flutter_dotenv: ^5.1.0
# State Management # State Management
flutter_riverpod: ^3.1.0 flutter_riverpod: ^3.1.0
@ -69,6 +70,9 @@ flutter:
- assets/images/enemies/ - assets/images/enemies/
- assets/images/backgrounds/ - assets/images/backgrounds/
- assets/audio/ - assets/audio/
- .env
- .env.development
- .env.production
# fonts: # fonts:
# - family: PixelFont # - family: PixelFont