login design added

This commit is contained in:
Surendiran 2025-09-02 10:59:26 +05:30
parent 92da28e6db
commit dbddbed8ce
10 changed files with 623 additions and 127 deletions

BIN
assets/login/login-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -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<Map<String, dynamic>> _makeGetRequest(
Uri url, Map<String, String> headers) async {
final response = await http.get(url, headers: headers);
return _handleResponse(response);
}
Future<Map<String, dynamic>> _makePostRequest(
Uri url, Map<String, String> body, Map<String, String> headers) async {
final response = await http.post(url, headers: headers, body: body);
return _handleResponse(response);
}
Future<Map<String, dynamic>> _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');
}
}
}

View File

@ -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<LoginScreen> {
final _formKey = GlobalKey<FormState>();
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<void> _handleLogin() async {
// Future<void> _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<void> 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<Color>(
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<Color>(
// 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?"),
// ),
// ],
// ),
// ),
// ),
// ),
// );
// }

View File

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

View File

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