diff --git a/assets/login/login-bg.png b/assets/login/login-bg.png new file mode 100644 index 0000000..0cf7a14 Binary files /dev/null and b/assets/login/login-bg.png differ diff --git a/assets/login/login-content.png b/assets/login/login-content.png new file mode 100644 index 0000000..5a0f86e Binary files /dev/null and b/assets/login/login-content.png differ diff --git a/assets/login/nhance-partner-logo.png b/assets/login/nhance-partner-logo.png new file mode 100644 index 0000000..5baf3f1 Binary files /dev/null and b/assets/login/nhance-partner-logo.png differ diff --git a/build/flutter_assets/assets/login/login-bg.png b/build/flutter_assets/assets/login/login-bg.png new file mode 100644 index 0000000..0cf7a14 Binary files /dev/null and b/build/flutter_assets/assets/login/login-bg.png differ diff --git a/build/flutter_assets/assets/login/login-content.png b/build/flutter_assets/assets/login/login-content.png new file mode 100644 index 0000000..5a0f86e Binary files /dev/null and b/build/flutter_assets/assets/login/login-content.png differ diff --git a/build/flutter_assets/assets/login/nhance-partner-logo.png b/build/flutter_assets/assets/login/nhance-partner-logo.png new file mode 100644 index 0000000..5baf3f1 Binary files /dev/null and b/build/flutter_assets/assets/login/nhance-partner-logo.png differ diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index e69de29..ba6ef37 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:nhance_app_pwa/customAppBar/toastHelper.dart'; +import 'package:nhance_app_pwa/models/environment.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class ApiService { + final BuildContext context; + + + + Future> _makeGetRequest( + Uri url, Map headers) async { + final response = await http.get(url, headers: headers); + return _handleResponse(response); + } + + Future> _makePostRequest( + Uri url, Map body, Map headers) async { + final response = await http.post(url, headers: headers, body: body); + return _handleResponse(response); + } + + Future> _handleResponse(http.Response response) async { + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else if (response.statusCode == 401) { + await _clearLocalStorageAndRedirect(); + return {}; + } else { + throw Exception('Failed to load data'); + } + } +} diff --git a/lib/presentation/screens/login/login_screen.dart b/lib/presentation/screens/login/login_screen.dart index 1346eeb..1eb9414 100644 --- a/lib/presentation/screens/login/login_screen.dart +++ b/lib/presentation/screens/login/login_screen.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:nhance_partner/data/utils/validators.dart'; +import 'package:pinput/pinput.dart'; import '../../../core/config/env.dart'; import '../../../core/routing/routes.dart'; import '../../../data/services/auth_service.dart'; +import '../../layouts/responsive_layout.dart'; import '../../themes/indicators/text_field_theme.dart'; class LoginScreen extends StatefulWidget { @@ -17,151 +20,593 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final _formKey = GlobalKey(); final TextEditingController _emailController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - + final TextEditingController _PhoneNumberController = TextEditingController(); + final TextEditingController _otpController = TextEditingController(); bool _isLoading = false; bool _obscurePassword = true; + int stausVal = 1; + int selectedIndex = 1; + bool enteredEmailOrMobile = false; - Future _handleLogin() async { + + // Future _handleLogin() async { + // if (!_formKey.currentState!.validate()) return; + // + // setState(() => _isLoading = true); + // + // // Simulate API call + // await Future.delayed(const Duration(seconds: 2)); + // + // // TODO: Replace with your AuthService + // final success = + // _emailController.text == "test@example.com" && + // // _passwordController.text == "password"; + // + // // setState(() => _isLoading = false); + // + // print("Succes - $success"); + // if (success) { + // print("Ckeck 1"); + // if (mounted) { + // print("Ckeck 2"); + // await AuthService.saveToken('1234'); + // final dynamic token = AuthService.saveToken('1234'); + // print('token - $token'); + // + // print('AppRoutes.home'); + // context.go(AppRoutes.home); + // print("Ckeck 3"); + // } + // } else { + // if (mounted) { + // ScaffoldMessenger.of( + // context, + // ).showSnackBar(const SnackBar(content: Text("Invalid credentials"))); + // } + // } + // } + + final defaultPinTheme = PinTheme( + width: 56, + height: 56, + textStyle: TextStyle( + fontSize: 20, + color: Color.fromRGBO(30, 60, 87, 1), + fontWeight: FontWeight.w600, + ), + decoration: BoxDecoration( + border: Border.all(color: Color.fromRGBO(234, 239, 243, 1)), + borderRadius: BorderRadius.circular(20), + ), + ); + + Future sendMobileOrEmailVerify() async { if (!_formKey.currentState!.validate()) return; - - setState(() => _isLoading = true); - - // Simulate API call - await Future.delayed(const Duration(seconds: 2)); - - // TODO: Replace with your AuthService - final success = - _emailController.text == "test@example.com" && - _passwordController.text == "password"; - - setState(() => _isLoading = false); - - print("Succes - $success"); - if (success) { - print("Ckeck 1"); - if (mounted) { - print("Ckeck 2"); - await AuthService.saveToken('1234'); - final dynamic token = AuthService.saveToken('1234'); - print('token - $token'); - - print('AppRoutes.home'); - context.go(AppRoutes.home); - print("Ckeck 3"); - } - } else { - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text("Invalid credentials"))); - } - } + setState(() { + enteredEmailOrMobile = true; + }); } - @override Widget build(BuildContext context) { - final isWeb = MediaQuery.of(context).size.width > 600; return Scaffold( - body: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 400), - padding: const EdgeInsets.all(16.0), - decoration: isWeb - ? BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 12, - color: Colors.black.withOpacity(0.1), + backgroundColor: Colors.white, + body: LayoutBuilder( + builder: (context, constraints) { + if (!ResponsiveLayout.isMobile(context)) { + // Web layout + return Row( + children: [ + Expanded( + child: Container( + color: Colors.white, + padding: const EdgeInsets.all(40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + "assets/login/nhance-partner-logo.png", + height: 45, + ), + const SizedBox(height: 40), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Welcome Back", + style: TextStyle( + fontSize: 50, + fontWeight: FontWeight.w600, + color: Color(0xFF425C5C), + ), + ), + const SizedBox(height: 20), + Image.asset( + "assets/login/login-content.png", + height: 500, + ), + const SizedBox(height: 10), + const Text( + '${'"'}Manage vsdv your policies, track quotations, and grow your business all in one place.${'"'}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + color: Color(0xFF000000), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], ), - ], - ) - : null, + ), + ), + // Right: Login form + Expanded( + child: Container( + // padding: const EdgeInsets.all(40), + decoration: const BoxDecoration( + color: Colors.white, + image: DecorationImage( + image: AssetImage("assets/login/login-bg.png"), // <-- your image path + fit: BoxFit.cover, // cover entire container + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(40), + bottomLeft: Radius.circular(40), + ), + ), + child: Center( // <-- center vertically + horizontally + child: SizedBox( + width: 600, // keep fixed width like first design + child: _LoginForm(context), + ), + ), + ), + ), + ], + ); + } else { + // Mobile layout + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 30), + Image.asset( + "assets/login/nhance-partner-logo.png", + height: 45, + ), + const SizedBox(height: 30), + Text( + "Welcome Back", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Color(0xFF425C5C), + ), + ), + const SizedBox(height: 20), + Image.asset( + "assets/login/login-content.png", + height: 180 , + ), + const SizedBox(height: 10), + Container( + padding: EdgeInsets.symmetric(vertical: 20,horizontal: 50 ), + child: Text( + '${'"'}Manage vsdv your policies, track quotations, and grow your business all in one place.${'"'}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11, + color: Color(0xFF000000), + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: MediaQuery.of(context).size.height, // full screen height + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF466F6C), Color(0xFF86D5CF)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(40), + topRight: Radius.circular(40), + ), + ), + child: _LoginForm(context), + ) + ], + ), + ); + } + }, + ), + ); + } + + Widget _LoginForm(BuildContext context) { + const focusedBorderColor = Colors.white; + const fillColor = Color.fromRGBO(243, 246, 249, 0); + const borderColor = Color.fromRGBO(23, 171, 144, 0.4); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Login to your account", + style: TextStyle( + fontSize: ResponsiveLayout.isMobile(context) ? 28 : 35, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + const SizedBox(height: 10), + Text( + "Enter your email or mobile number to receive a one time password.", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: ResponsiveLayout.isMobile(context) ? 14 : 18, + color: Colors.white, + fontWeight: FontWeight.w600 + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(50), + ), + + child: Row( + children: [ + buildTab("Mobile", 0), + const SizedBox(width: 10), + buildTab("Email", 1), + ], + ), + ), + ], + ), + const SizedBox(height: 30), + Container( child: Form( key: _formKey, child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisSize: MainAxisSize.max, children: [ - // App Logo - Image.asset('assets/logo.png', width: 80, height: 80), - const SizedBox(height: 20), - - // Environment label - Text( - "Environment: ${Env.envName.toUpperCase()}", - style: const TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(height: 20), - - ThemedFormField( - hintText: 'Email', - backgroundColor: Colors.white, - validator: (value) => Validators.email(value, "Email"), - // imgPath: "MiscIconAssetPath.person", - controller: _emailController, - // txtwidth: MediaQuery.of(context).size.width * 0.5, - // keyboardType: TextInputType.number, - // maxLength: 10, - // inputFormatters: [FilteringTextInputFormatter.digitsOnly], - // readOnly: true, - ), - - const SizedBox(height: 16), - ThemedFormField( - hintText: 'Password', - backgroundColor: Colors.white, - validator: (value) => - Validators.passwordlogIn(value, "Password"), - // imgPath: "MiscIconAssetPath.person", - controller: _passwordController, - isObscurable: true, - // txtwidth: MediaQuery.of(context).size.width * 0.5, - // keyboardType: TextInputType.number, - // maxLength: 10, - // inputFormatters: [FilteringTextInputFormatter.digitsOnly], - // readOnly: true, - ), - - // Password - const SizedBox(height: 24), - - // Login Button SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _isLoading ? null : _handleLogin, - child: _isLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), - ), - ) - : const Text("Login"), + width: 400, + height: 70, + child: Column( + children: [ + if (stausVal == 0) + ThemedFormField( + hintText: 'Email', + backgroundColor: Colors.white, + validator: (value) => Validators.email(value, "Email"), + // imgPath: "MiscIconAssetPath.person", + controller: _emailController, + ), + if (stausVal == 1) + ThemedFormField( + hintText: 'Mobile Number', + backgroundColor: Colors.white, + validator: (value) => Validators.phone(value, "Phone"), + // imgPath: "MiscIconAssetPath.person", + controller: _PhoneNumberController, + // txtwidth: MediaQuery.of(context).size.width * 0.5, + keyboardType: TextInputType.number, + maxLength: 10, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + // readOnly: true, + ), + // const SizedBox(height: 16), + // ThemedFormField( + // hintText: 'Password', + // backgroundColor: Colors.white, + // validator: (value) => + // Validators.passwordlogIn(value, "Password"), + // // imgPath: "MiscIconAssetPath.person", + // controller: _passwordController, + // isObscurable: true, + // // txtwidth: MediaQuery.of(context).size.width * 0.5, + // // keyboardType: TextInputType.number, + // // maxLength: 10, + // // inputFormatters: [FilteringTextInputFormatter.digitsOnly], + // // readOnly: true, + // ), + ], + ) + ), + const SizedBox(height: 20), + if(enteredEmailOrMobile) + Container( + child: Text('OTP', + style: TextStyle( + fontSize: ResponsiveLayout.isMobile(context) ? 14 : 18, + fontWeight: FontWeight.w600, + color: Colors.white + ), ), ), - - const SizedBox(height: 12), - - // Forgot password link - TextButton( - onPressed: () { - // TODO: Navigate to forgot password screen - }, - child: const Text("Forgot password?"), + if(enteredEmailOrMobile) + Container( + child: Pinput( + length: 6, + defaultPinTheme: + defaultPinTheme, + separatorBuilder: + (index) => + const SizedBox( + width: 8), + showCursor: true, + controller: _otpController, + // validator: _validatePin, + errorPinTheme: + defaultPinTheme + .copyBorderWith( + border: Border.all( + color: Colors + .redAccent), + ), + hapticFeedbackType: + HapticFeedbackType + .lightImpact, + onCompleted: (pin) { + debugPrint( + 'onCompleted: $pin'); + }, + onChanged: (value) { + debugPrint( + 'onChanged: $value'); + }, + cursor: Column( + mainAxisAlignment: + MainAxisAlignment + .end, + children: [ + Container( + margin: + const EdgeInsets + .only( + bottom: 9), + width: 22, + height: 1, + color: + focusedBorderColor, + ), + ], + ), + focusedPinTheme: + defaultPinTheme + .copyWith( + decoration: + defaultPinTheme + .decoration! + .copyWith( + borderRadius: + BorderRadius + .circular(8), + border: Border.all( + color: + focusedBorderColor), + ), + ), + submittedPinTheme: + defaultPinTheme + .copyWith( + decoration: + defaultPinTheme + .decoration! + .copyWith( + color: fillColor, + borderRadius: + BorderRadius + .circular(19), + border: Border.all( + color: + focusedBorderColor), + ), + ), + ), ), + const SizedBox(height: 20), + SizedBox( + width: 400, + child: SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF436462), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () { + sendMobileOrEmailVerify(); + }, + child: Text( + enteredEmailOrMobile ? 'Continue' : 'Request OTP', + style: TextStyle( + fontSize: ResponsiveLayout.isMobile(context) ? 18 : 20, + fontWeight: FontWeight.w600, + color: Colors.white + ), + ), + ), + ), + ), + const SizedBox(height: 20), ], ), ), ), - ), + ], + ); + } + + Widget buildTab(String title, int index) { + final isSelected = selectedIndex == index + 1; + return GestureDetector( + onTap: () { + setState(() { + selectedIndex = index + 1; + if (selectedIndex == 2) { + stausVal = 0; + } else { + stausVal = 1; + } + }); + }, + child: Container( + padding:EdgeInsets.only( + top: 6.5, bottom: 6.0, left: 40.0, right: 40.0), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF436462) : Colors.transparent, + boxShadow: isSelected + ? [ + BoxShadow( + color: isSelected + ? const Color(0xFF436462) + : Colors.transparent, // Grey shadow + spreadRadius: 0.2, + blurRadius: 1, + offset: const Offset(0, 1), // Horizontal, Vertical + ), + ] + : [], + borderRadius: BorderRadius.circular(16), + ), + child: Text(title, + style: GoogleFonts.poppins( + fontSize: 12, + color: isSelected ? Colors.white : Colors.black, + fontWeight: FontWeight.w500, + ))), ); } } + + + // + // @override + // Widget build(BuildContext context) { + // final isWeb = MediaQuery.of(context).size.width > 600; + // + // return Scaffold( + // body: Center( + // child: Container( + // constraints: const BoxConstraints(maxWidth: 400), + // padding: const EdgeInsets.all(16.0), + // decoration: isWeb + // ? BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(8), + // boxShadow: [ + // BoxShadow( + // blurRadius: 12, + // color: Colors.black.withOpacity(0.1), + // ), + // ], + // ) + // : null, + // child: Form( + // key: _formKey, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // children: [ + // // App Logo + // Image.asset('assets/logo.png', width: 80, height: 80), + // const SizedBox(height: 20), + // + // // Environment label + // Text( + // "Environment: ${Env.envName.toUpperCase()}", + // style: const TextStyle(fontSize: 14, color: Colors.grey), + // ), + // const SizedBox(height: 20), + // + // ThemedFormField( + // hintText: 'Email', + // backgroundColor: Colors.white, + // validator: (value) => Validators.email(value, "Email"), + // // imgPath: "MiscIconAssetPath.person", + // controller: _emailController, + // // txtwidth: MediaQuery.of(context).size.width * 0.5, + // // keyboardType: TextInputType.number, + // // maxLength: 10, + // // inputFormatters: [FilteringTextInputFormatter.digitsOnly], + // // readOnly: true, + // ), + // + // const SizedBox(height: 16), + // ThemedFormField( + // hintText: 'Password', + // backgroundColor: Colors.white, + // validator: (value) => + // Validators.passwordlogIn(value, "Password"), + // // imgPath: "MiscIconAssetPath.person", + // controller: _passwordController, + // isObscurable: true, + // // txtwidth: MediaQuery.of(context).size.width * 0.5, + // // keyboardType: TextInputType.number, + // // maxLength: 10, + // // inputFormatters: [FilteringTextInputFormatter.digitsOnly], + // // readOnly: true, + // ), + // + // // Password + // const SizedBox(height: 24), + // + // // Login Button + // SizedBox( + // width: double.infinity, + // child: ElevatedButton( + // onPressed: _isLoading ? null : _handleLogin, + // child: _isLoading + // ? const SizedBox( + // height: 20, + // width: 20, + // child: CircularProgressIndicator( + // strokeWidth: 2, + // valueColor: AlwaysStoppedAnimation( + // Colors.white, + // ), + // ), + // ) + // : const Text("Login"), + // ), + // ), + // + // const SizedBox(height: 12), + // + // // Forgot password link + // TextButton( + // onPressed: () { + // // TODO: Navigate to forgot password screen + // }, + // child: const Text("Forgot password?"), + // ), + // ], + // ), + // ), + // ), + // ), + // ); + // } + diff --git a/pubspec.lock b/pubspec.lock index b05dae4..b8fc766 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -592,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" + url: "https://pub.dev" + source: hosted + version: "5.0.1" platform: dependency: transitive description: @@ -821,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87eba71..8fd4cd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: google_fonts: ^6.3.1 intl: ^0.20.2 flutter_hooks: ^0.21.3+1 + pinput: ^5.0.1 dev_dependencies: flutter_test: @@ -66,11 +67,10 @@ dev_dependencies: # The following section is specific to Flutter packages. 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. + generate: true uses-material-design: true + assets: + - assets/login/ # To add assets to your application, add an assets section, like this: # assets: