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 _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<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) return;
FocusScope.of(context).unfocus();
setState(() => _errorMessage = null);
if (!_formKey.currentState!.validate()) {
return;
}
setState(() => _isLoading = true);
@ -43,38 +55,43 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
}
} 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.';
setState(() {
_isLoading = false;
_errorMessage = _parseErrorMessage(e.toString());
});
}
}
}
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
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
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,
),
child: IntrinsicHeight(
child: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
@ -82,12 +99,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
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,
@ -95,7 +122,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
color: Colors.black,
),
),
const SizedBox(height: 24),
const SizedBox(height: 32),
Text(
'WELCOME BACK',
style: Theme.of(context).textTheme.displayMedium,
@ -108,13 +136,53 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
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';
@ -126,9 +194,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
},
),
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),
@ -139,10 +212,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
: Icons.visibility_off_outlined,
),
onPressed: () {
setState(() => _obscurePassword = !_obscurePassword);
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
onFieldSubmitted: (_) => _handleLogin(),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
@ -154,8 +230,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
},
),
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,
@ -165,9 +248,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
color: Colors.black,
),
)
: const Text('LOGIN'),
: const Text(
'LOGIN',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -176,11 +267,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
style: Theme.of(context).textTheme.bodyMedium,
),
TextButton(
onPressed: () => context.go('/register'),
onPressed: _isLoading
? null
: () => context.go('/register'),
child: const Text('REGISTER'),
),
],
),
const Spacer(),
],
),
),
@ -188,5 +283,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
),
);
},
),
),
),
);
}
}

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_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<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
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,15 +219,26 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
onPressed: () => context.go('/login'),
),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
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,
),
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,
@ -72,11 +253,15 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
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';
@ -87,64 +272,21 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
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'),
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(
@ -160,6 +302,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
),
],
),
const Spacer(),
],
),
),
@ -167,5 +310,10 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
),
),
);
},
),
),
),
);
}
}

View file

@ -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:

View file

@ -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