diff --git a/lib/features/authorization/application/authorization_application.dart b/lib/features/authorization/application/authorization_application.dart index d94a68b..9f9e9b4 100644 --- a/lib/features/authorization/application/authorization_application.dart +++ b/lib/features/authorization/application/authorization_application.dart @@ -63,9 +63,13 @@ class AuthorizationController extends AsyncNotifier { final accountsGrpc = ref.read(publicAccountsGrpcProvider); final tokenCtrl = ref.read(tokensControllerProvider.notifier); - final response = await accountsGrpc.signIn(form.toProto()); - await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair)); - return state.value!.copyWith(isAuthorized: true); + try { + final response = await accountsGrpc.signIn(form.toProto()); + await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair)); + return state.value!.copyWith(isAuthorized: true); + } catch (e) { + rethrow; + } }); } diff --git a/lib/features/authorization/presentation/authorization_page.dart b/lib/features/authorization/presentation/authorization_page.dart index 6c30f38..7acf921 100644 --- a/lib/features/authorization/presentation/authorization_page.dart +++ b/lib/features/authorization/presentation/authorization_page.dart @@ -12,17 +12,22 @@ class AuthorizationPage extends ConsumerStatefulWidget { ConsumerState createState() => _AuthorizationPage(); } +enum AuthMode { login, signup } + class _AuthorizationPage extends ConsumerState { + bool showSignUp = false; + + void toggleAuth() { + setState(() { + showSignUp = !showSignUp; + }); + } + @override Widget build(BuildContext context) { - final state = ref.watch(authorizationControllerProvider); - final isLoading = state.isLoading; - final authState = state.value; - return LayoutBuilder( builder: (context, constraints) { final showDecoration = constraints.maxWidth > 1000; - return Scaffold( body: Row( children: [ @@ -37,10 +42,14 @@ class _AuthorizationPage extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (authState!.mode == AuthMode.login) - LoginForm() - else - RegisterForm(), + Offstage( + offstage: showSignUp == true, + child: LoginForm(toggleAuth: toggleAuth), + ), + Offstage( + offstage: showSignUp != true, + child: RegisterForm(toggleAuth: toggleAuth), + ), ], ), ), @@ -49,13 +58,6 @@ class _AuthorizationPage extends ConsumerState { // ✅ only show decoration if width > 400 if (showDecoration) Expanded(flex: 3, child: AuthDecoration()), - if (isLoading) - Positioned.fill( - child: Container( - color: Colors.black.withValues(alpha: 0.2), - child: const Center(child: CircularProgressIndicator()), - ), - ), ], ), ); diff --git a/lib/features/authorization/presentation/decoration.dart b/lib/features/authorization/presentation/decoration.dart index d350e6f..19145b3 100644 --- a/lib/features/authorization/presentation/decoration.dart +++ b/lib/features/authorization/presentation/decoration.dart @@ -1,89 +1,91 @@ -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:softplayer_web/shared/ui/colors/light_mode.dart'; class AuthDecoration extends StatelessWidget { - final double spacing; - final Color color; - - const AuthDecoration({ - super.key, - this.spacing = 24, - this.color = Colors.blue, - }); + const AuthDecoration({super.key}); @override Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - return Container( - color: LightModeColors.backgroundElevated, - child: FractionallySizedBox( - widthFactor: 0.3, - alignment: Alignment.centerLeft, - child: CustomPaint( - painter: _GridPainter(spacing: spacing, color: color), - child: const SizedBox.expand(), - ), - ), - ); - }, + return Container( + color: LightModeColors.backgroundElevated, + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + child: const CustomPaint( + painter: _GradientPainter(), + child: SizedBox.expand(), + ), + ), ); } } -// todo: Some AI generated shite, that doesn't work -class _GridPainter extends CustomPainter { - final double spacing; - final Color color; - - _GridPainter({required this.spacing, required this.color}); - - static const double maxSquare = 85; - static const double minSquare = 48; // 2x gap +class _GradientPainter extends CustomPainter { + const _GradientPainter(); @override void paint(Canvas canvas, Size size) { - final paint = Paint()..color = color; + // 🎨 Main smooth background gradient + final rect = Offset.zero & size; - // ✅ outer padding included in layout math - final innerWidth = size.width - spacing * 2; - final innerHeight = size.height - spacing * 2; + final backgroundGradient = const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0F172A), // deep navy + Color(0xFF111827), // dark slate + ], + ); - int columns = 10; + final paint = Paint()..shader = backgroundGradient.createShader(rect); - double calcSize(int cols) { - return (innerWidth - (cols - 1) * spacing) / cols; - } + canvas.drawRect(rect, paint); - // choose columns safely - while (columns > 1 && calcSize(columns) < minSquare) { - columns--; - } + // 🌈 soft glowing blobs (adds "cool gradient feel") + _drawGlow( + canvas, + size, + offset: const Offset(0.2, 0.3), + radius: size.width * 0.8, + colors: [Colors.cyan.withOpacity(0.25), Colors.transparent], + ); - double square = calcSize(columns); - square = math.min(square, maxSquare); + _drawGlow( + canvas, + size, + offset: const Offset(0.8, 0.6), + radius: size.width * 0.9, + colors: [Colors.purple.withOpacity(0.20), Colors.transparent], + ); - final rows = (innerHeight / (square + spacing)).ceil(); + _drawGlow( + canvas, + size, + offset: const Offset(0.5, 0.9), + radius: size.width * 0.7, + colors: [Colors.blue.withOpacity(0.20), Colors.transparent], + ); + } - for (int row = 0; row < rows; row++) { - final y = spacing + row * (square + spacing); + void _drawGlow( + Canvas canvas, + Size size, { + required Offset offset, + required double radius, + required List colors, + }) { + final center = Offset(size.width * offset.dx, size.height * offset.dy); - for (int col = 0; col < columns; col++) { - final x = spacing + col * (square + spacing); + final gradient = RadialGradient(colors: colors, radius: 1.0); - final rect = Rect.fromLTWH(x, y, square, square); + final paint = Paint() + ..shader = gradient.createShader( + Rect.fromCircle(center: center, radius: radius), + ) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 60); - canvas.drawRRect( - RRect.fromRectAndRadius(rect, const Radius.circular(6)), - paint, - ); - } - } + canvas.drawCircle(center, radius, paint); } @override - bool shouldRepaint(covariant _GridPainter oldDelegate) { - return oldDelegate.spacing != spacing || oldDelegate.color != color; - } + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } diff --git a/lib/features/authorization/presentation/login_form.dart b/lib/features/authorization/presentation/login_form.dart index ec0836b..eb33597 100644 --- a/lib/features/authorization/presentation/login_form.dart +++ b/lib/features/authorization/presentation/login_form.dart @@ -1,11 +1,15 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; import 'package:softplayer_web/features/authorization/application/sign_in_data.dart'; +import 'package:toastification/toastification.dart'; class LoginForm extends ConsumerStatefulWidget { - const LoginForm({super.key}); + const LoginForm({super.key, required this.toggleAuth}); + final VoidCallback toggleAuth; @override ConsumerState createState() => _LoginForm(); } @@ -16,21 +20,36 @@ class _LoginForm extends ConsumerState { final TextEditingController emailCtrl = TextEditingController(); final TextEditingController passwordCtrl = TextEditingController(); - void _submitForm() { + Future _submitForm() async { if (_formKey.currentState!.validate()) { // If valid, you can use the values final email = emailCtrl.text; final password = passwordCtrl.text; final form = SignInData(email: email, password: password); - ref.read(authorizationControllerProvider.notifier).signin(form); + try { + await ref.read(authorizationControllerProvider.notifier).signin(form); + } catch (e) { + if (!mounted) { + return; + } + log(e.toString()); + toastification.show( + context: context, + type: ToastificationType.error, + style: ToastificationStyle.flatColored, + alignment: Alignment.topRight, + autoCloseDuration: const Duration(seconds: 4), + title: const Text('Authentication failed'), + description: Text(e.toString()), + showProgressBar: false, + ); + } } } @override Widget build(BuildContext context) { - final state = ref.watch(authorizationControllerProvider); final controller = ref.read(authorizationControllerProvider.notifier); - final isLoading = state.isLoading; return SizedBox( width: 400, child: Form( @@ -54,7 +73,7 @@ class _LoginForm extends ConsumerState { style: Theme.of(context).textTheme.bodyMedium, ), TextButton( - onPressed: controller.toggleAuthMode, + onPressed: widget.toggleAuth, style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size(0, 0), @@ -72,48 +91,26 @@ class _LoginForm extends ConsumerState { TextFormField( onFieldSubmitted: (_) => _submitForm(), controller: emailCtrl, + decoration: InputDecoration(hintText: "Email address"), validator: (value) { - if (value == null || value.trim().isEmpty) { + if (value == null || value.isEmpty) { return 'Email is required'; } - final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegex.hasMatch(value.trim())) { - return 'Enter a valid email address'; - } return null; }, - decoration: InputDecoration(hintText: "Email address"), ), SizedBox(height: 16), TextFormField( onFieldSubmitted: (_) => _submitForm(), controller: passwordCtrl, + obscureText: true, + decoration: InputDecoration(hintText: "Password"), validator: (value) { if (value == null || value.isEmpty) { return 'Password is required'; } - - if (value.length < 12) { - return 'Password must be at least 12 characters long'; - } - - final hasNumber = RegExp(r'\d'); - final hasSpecialChar = RegExp( - r'[!@#$%^&*(),.?":{}|<>_\-\\[\]\\/+=~`]', - ); - - if (!hasNumber.hasMatch(value)) { - return 'Password must contain at least one number'; - } - - if (!hasSpecialChar.hasMatch(value)) { - return 'Password must contain at least one special character'; - } - return null; }, - obscureText: true, - decoration: InputDecoration(hintText: "Password"), ), SizedBox(height: 16), SizedBox( @@ -121,13 +118,6 @@ class _LoginForm extends ConsumerState { child: Text("Forgot password?", textAlign: TextAlign.left), ), ElevatedButton(onPressed: _submitForm, child: const Text('Log in')), - if (isLoading) - Positioned.fill( - child: Container( - color: Colors.black.withValues(alpha: 0.2), - child: const Center(child: CircularProgressIndicator()), - ), - ), ], ), ), diff --git a/lib/features/authorization/presentation/register_form.dart b/lib/features/authorization/presentation/register_form.dart index ffc702b..8d012aa 100644 --- a/lib/features/authorization/presentation/register_form.dart +++ b/lib/features/authorization/presentation/register_form.dart @@ -4,8 +4,9 @@ import 'package:softplayer_web/features/authorization/application/authorization_ import 'package:softplayer_web/features/authorization/application/sign_up_data.dart'; class RegisterForm extends ConsumerStatefulWidget { - const RegisterForm({super.key}); + const RegisterForm({super.key, required this.toggleAuth}); + final VoidCallback toggleAuth; @override ConsumerState createState() => _RegisterForm(); } @@ -61,7 +62,7 @@ class _RegisterForm extends ConsumerState { style: Theme.of(context).textTheme.bodyMedium, ), TextButton( - onPressed: controller.toggleAuthMode, + onPressed: widget.toggleAuth, style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size(0, 0), @@ -80,18 +81,41 @@ class _RegisterForm extends ConsumerState { controller: nameCtrl, onFieldSubmitted: (_) => _submitForm(), decoration: InputDecoration(hintText: "Name"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + return null; + }, ), SizedBox(height: 16), TextFormField( controller: surnameCtrl, onFieldSubmitted: (_) => _submitForm(), decoration: InputDecoration(hintText: "Surname"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + + return null; + }, ), SizedBox(height: 16), TextFormField( controller: emailCtrl, onFieldSubmitted: (_) => _submitForm(), decoration: InputDecoration(hintText: "Email address"), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Email is required'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value.trim())) { + return 'Enter a valid email address'; + } + return null; + }, ), SizedBox(height: 16), TextFormField( @@ -99,6 +123,30 @@ class _RegisterForm extends ConsumerState { onFieldSubmitted: (_) => _submitForm(), obscureText: true, decoration: InputDecoration(hintText: "Password"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + + if (value.length < 12) { + return 'Password must be at least 12 characters long'; + } + + final hasNumber = RegExp(r'\d'); + final hasSpecialChar = RegExp( + r'[!@#$%^&*(),.?":{}|<>_\-\\[\]\\/+=~`]', + ); + + if (!hasNumber.hasMatch(value)) { + return 'Password must contain at least one number'; + } + + if (!hasSpecialChar.hasMatch(value)) { + return 'Password must contain at least one special character'; + } + + return null; + }, ), SizedBox(height: 16), TextFormField( @@ -106,7 +154,19 @@ class _RegisterForm extends ConsumerState { onFieldSubmitted: (_) => _submitForm(), obscureText: true, decoration: InputDecoration(hintText: "Repeat the password"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + + if (value != passwordCtrl.text) { + return 'Passwords do not match'; + } + + return null; + }, ), + SizedBox(height: 16), ElevatedButton( onPressed: _submitForm, child: const Text('Sign up'), diff --git a/lib/shared/ui/colors/light_mode.dart b/lib/shared/ui/colors/light_mode.dart index 294b9de..f872fbf 100644 --- a/lib/shared/ui/colors/light_mode.dart +++ b/lib/shared/ui/colors/light_mode.dart @@ -11,6 +11,7 @@ class LightModeColors { // Inputs static const inputBackground = Color(0xFFFFFFFF); static const inputDefaultBorder = Color(0xFFD9D9D9); + static const inputHoverBorder = Color(0xFFC9CED6); static const inputFocusedBorder = Color(0xFFEFFF1A); // Text diff --git a/lib/shared/ui/theme/app_theme.dart b/lib/shared/ui/theme/app_theme.dart index 0da05f7..f9295bb 100644 --- a/lib/shared/ui/theme/app_theme.dart +++ b/lib/shared/ui/theme/app_theme.dart @@ -72,10 +72,11 @@ class AppTheme { width: 1.0, ), ), + hoverColor: LightModeColors.inputBackground, enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), borderSide: BorderSide( - color: LightModeColors.inputDefaultBorder, + color: LightModeColors.inputHoverBorder, width: 1.0, ), ), diff --git a/pubspec.lock b/pubspec.lock index 67ff09e..98d462a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -544,6 +552,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" platform: dependency: transitive description: @@ -734,6 +750,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.17" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "66c96678e3dece8ba24de3ea31634bd65a80aaecb8105f9bafe946e5f0d7590a" + url: "https://pub.dev" + source: hosted + version: "3.2.0" typed_data: dependency: transitive description: @@ -742,6 +766,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 038e2db..df1e051 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: go_router: ^17.2.3 flutter_secure_storage: ^10.2.0 jwt_decoder: ^2.0.1 + toastification: ^3.2.0 dev_dependencies: flutter_test: sdk: flutter