Implement forgot password functionality and enhance UI in the authentication flow
This commit is contained in:
parent
915471f4c0
commit
8b01161448
@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart';
|
|||||||
import 'package:marco/helpers/services/auth_service.dart';
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
import 'package:marco/helpers/widgets/my_validators.dart';
|
import 'package:marco/helpers/widgets/my_validators.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
class ForgotPasswordController extends MyController {
|
class ForgotPasswordController extends MyController {
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
MyFormValidator basicValidator = MyFormValidator();
|
||||||
bool showPassword = false;
|
bool showPassword = false;
|
||||||
@ -35,6 +35,31 @@ class ForgotPasswordController extends MyController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// New: Forgot password function
|
||||||
|
Future<void> onForgotPassword() async {
|
||||||
|
if (basicValidator.validateForm()) {
|
||||||
|
update();
|
||||||
|
final data = basicValidator.getData();
|
||||||
|
final email = data['email']?.toString() ?? '';
|
||||||
|
final result = await AuthService.forgotPassword(email);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Your password reset link was sent.",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Your password reset link was sent.",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void gotoLogIn() {
|
void gotoLogIn() {
|
||||||
Get.toNamed('/auth/login');
|
Get.toNamed('/auth/login');
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
import 'package:marco/helpers/services/storage/local_storage.dart';
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
import 'package:marco/controller/permission_controller.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
@ -112,4 +112,89 @@ class AuthService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forgot password API
|
||||||
|
static Future<Map<String, String>?> forgotPassword(String email) async {
|
||||||
|
final requestBody = {"email": email};
|
||||||
|
|
||||||
|
logger.i("Sending forgot password request with email: $email");
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse("$_baseUrl/auth/forgot-password"),
|
||||||
|
headers: _headers,
|
||||||
|
body: jsonEncode(requestBody),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.i(
|
||||||
|
"Forgot password API response (${response.statusCode}): ${response.body}");
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||||
|
logger.i("Forgot password request successful.");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
"error":
|
||||||
|
responseData['message'] ?? "Failed to send password reset link."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Exception during forgot password request: $e");
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request demo API
|
||||||
|
static Future<Map<String, String>?> requestDemo(
|
||||||
|
Map<String, dynamic> demoData) async {
|
||||||
|
try {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse("$_baseUrl/market/inquiry"),
|
||||||
|
headers: _headers,
|
||||||
|
body: jsonEncode(demoData),
|
||||||
|
);
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||||
|
logger.i("Request Demo submitted successfully.");
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
"error": responseData['message'] ?? "Failed to submit demo request."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Exception during request demo: $e");
|
||||||
|
return {"error": "Network error. Please check your connection."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, dynamic>>?> getIndustries() async {
|
||||||
|
try {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse("$_baseUrl/market/industries"),
|
||||||
|
headers: _headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.i(
|
||||||
|
"Get Industries API response (${response.statusCode}): ${response.body}");
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 && responseData['success'] == true) {
|
||||||
|
// Return the list of industries as List<Map<String, dynamic>>
|
||||||
|
final List<dynamic> industriesData = responseData['data'];
|
||||||
|
return industriesData.cast<Map<String, dynamic>>();
|
||||||
|
} else {
|
||||||
|
logger.w("Failed to fetch industries: ${responseData['message']}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("Exception during getIndustries: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,9 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/view/layouts/auth_layout.dart';
|
import 'package:marco/view/layouts/auth_layout.dart';
|
||||||
|
import 'package:marco/images.dart';
|
||||||
|
import 'package:marco/helpers/theme/app_theme.dart';
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordScreen extends StatefulWidget {
|
class ForgotPasswordScreen extends StatefulWidget {
|
||||||
const ForgotPasswordScreen({super.key});
|
const ForgotPasswordScreen({super.key});
|
||||||
@ -26,57 +29,88 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> with UIMixi
|
|||||||
init: controller,
|
init: controller,
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
return Form(
|
return Form(
|
||||||
key: controller.basicValidator.formKey,
|
key: controller.basicValidator.formKey,
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
padding: MySpacing.xy(2, 40),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Container(
|
||||||
children: [
|
width: double.infinity,
|
||||||
MyText.titleLarge("Forgot Password", fontWeight: 600),
|
padding: MySpacing.all(24),
|
||||||
MySpacing.height(12),
|
decoration: BoxDecoration(
|
||||||
MyText.bodyMedium(
|
color: theme.colorScheme.primary.withOpacity(0.02),
|
||||||
"Enter the email address associated with your account and we'll send an email instructions on how to recover your password.",
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: contentTheme.primary.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Image.asset(
|
||||||
|
Images.logoDark,
|
||||||
|
height: 120,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(10),
|
||||||
|
MyText.titleLarge("Forgot Password", fontWeight: 600),
|
||||||
|
MySpacing.height(12),
|
||||||
|
MyText.bodyMedium(
|
||||||
|
"Enter your email and we'll send you instructions to reset your password.",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
xMuted: true),
|
xMuted: true,
|
||||||
MySpacing.height(12),
|
),
|
||||||
TextFormField(
|
MySpacing.height(12),
|
||||||
|
TextFormField(
|
||||||
validator: controller.basicValidator.getValidation('email'),
|
validator: controller.basicValidator.getValidation('email'),
|
||||||
controller: controller.basicValidator.getController('email'),
|
controller: controller.basicValidator.getController('email'),
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
style: MyTextStyle.labelMedium(),
|
style: MyTextStyle.labelMedium(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: "Email Address",
|
labelText: "Email Address",
|
||||||
labelStyle: MyTextStyle.bodySmall(xMuted: true),
|
labelStyle: MyTextStyle.bodySmall(xMuted: true),
|
||||||
border: OutlineInputBorder(borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderSide: BorderSide.none),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: contentTheme.secondary.withAlpha(36),
|
fillColor: theme.cardColor,
|
||||||
prefixIcon: Icon(LucideIcons.mail, size: 16),
|
prefixIcon: Icon(LucideIcons.mail, size: 16),
|
||||||
contentPadding: MySpacing.all(15),
|
contentPadding: MySpacing.all(15),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
MySpacing.height(20),
|
||||||
MySpacing.height(20),
|
Center(
|
||||||
Center(
|
child: MyButton.rounded(
|
||||||
child: MyButton.rounded(
|
onPressed: controller.onForgotPassword,
|
||||||
onPressed: controller.onLogin,
|
elevation: 0,
|
||||||
elevation: 0,
|
padding: MySpacing.xy(20, 16),
|
||||||
padding: MySpacing.xy(20, 16),
|
backgroundColor: Colors.blueAccent,
|
||||||
backgroundColor: contentTheme.primary,
|
child: MyText.labelMedium(
|
||||||
child: MyText.labelMedium('Forgot Password', color: contentTheme.onPrimary),
|
'Send Reset Link',
|
||||||
|
color: contentTheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Center(
|
||||||
Center(
|
child: MyButton.text(
|
||||||
child: MyButton.text(
|
onPressed: controller.gotoLogIn,
|
||||||
onPressed: controller.gotoLogIn,
|
elevation: 0,
|
||||||
elevation: 0,
|
padding: MySpacing.x(16),
|
||||||
padding: MySpacing.x(16),
|
splashColor:
|
||||||
splashColor: contentTheme.secondary.withValues(alpha:0.1),
|
contentTheme.secondary.withValues(alpha: 0.1),
|
||||||
child: MyText.labelMedium('Back to log in', color: contentTheme.secondary),
|
child: MyText.labelMedium(
|
||||||
|
'Back to log in',
|
||||||
|
color: contentTheme.secondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
import 'package:marco/helpers/widgets/my_text_style.dart';
|
||||||
import 'package:marco/view/layouts/auth_layout.dart';
|
import 'package:marco/view/layouts/auth_layout.dart';
|
||||||
import 'package:marco/images.dart';
|
import 'package:marco/images.dart';
|
||||||
|
import 'package:marco/view/auth/request_demo_bottom_sheet.dart';
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({super.key});
|
const LoginScreen({super.key});
|
||||||
@ -194,7 +195,7 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
|
|||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: MySpacing.xy(24, 16),
|
padding: MySpacing.xy(24, 16),
|
||||||
borderRadiusAll: 16,
|
borderRadiusAll: 16,
|
||||||
backgroundColor: contentTheme.primary,
|
backgroundColor: Colors.blueAccent,
|
||||||
child: MyText.labelMedium(
|
child: MyText.labelMedium(
|
||||||
'Login',
|
'Login',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@ -207,10 +208,13 @@ class _LoginScreenState extends State<LoginScreen> with UIMixin {
|
|||||||
/// Register Link
|
/// Register Link
|
||||||
Center(
|
Center(
|
||||||
child: MyButton.text(
|
child: MyButton.text(
|
||||||
onPressed: controller.gotoRegister,
|
onPressed: () {
|
||||||
|
OrganizationFormBottomSheet.show(context);
|
||||||
|
},
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
padding: MySpacing.xy(12, 8),
|
padding: MySpacing.xy(12, 8),
|
||||||
splashColor: contentTheme.secondary.withAlpha(30),
|
splashColor:
|
||||||
|
contentTheme.secondary.withAlpha(30),
|
||||||
child: MyText.bodySmall(
|
child: MyText.bodySmall(
|
||||||
"Request a Demo",
|
"Request a Demo",
|
||||||
color: contentTheme.secondary,
|
color: contentTheme.secondary,
|
||||||
|
325
lib/view/auth/request_demo_bottom_sheet.dart
Normal file
325
lib/view/auth/request_demo_bottom_sheet.dart
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
||||||
|
import 'package:marco/helpers/services/auth_service.dart';
|
||||||
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
|
|
||||||
|
class OrganizationFormBottomSheet {
|
||||||
|
static void show(BuildContext context) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (_) => DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
initialChildSize: 0.85,
|
||||||
|
minChildSize: 0.5,
|
||||||
|
maxChildSize: 0.95,
|
||||||
|
builder: (context, scrollController) {
|
||||||
|
return _OrganizationForm(scrollController: scrollController);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrganizationForm extends StatefulWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
const _OrganizationForm({required this.scrollController});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OrganizationForm> createState() => _OrganizationFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrganizationFormState extends State<_OrganizationForm> {
|
||||||
|
final MyFormValidator validator = MyFormValidator();
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _industries = [];
|
||||||
|
String? _selectedIndustryId;
|
||||||
|
|
||||||
|
final List<String> _sizes = [
|
||||||
|
'1-10',
|
||||||
|
'11-50',
|
||||||
|
'51-200',
|
||||||
|
'201-1000',
|
||||||
|
'1000+',
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, String> _sizeApiMap = {
|
||||||
|
'1-10': 'less than 10',
|
||||||
|
'11-50': '11 to 50',
|
||||||
|
'51-200': '51 to 200',
|
||||||
|
'201-1000': 'more than 200',
|
||||||
|
'1000+': 'more than 1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
String? _selectedSize;
|
||||||
|
bool _agreed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_loadIndustries();
|
||||||
|
|
||||||
|
validator.addField<String>('organizationName',
|
||||||
|
required: true, controller: TextEditingController());
|
||||||
|
validator.addField<String>('email',
|
||||||
|
required: true, controller: TextEditingController());
|
||||||
|
validator.addField<String>('contactPerson',
|
||||||
|
required: true, controller: TextEditingController());
|
||||||
|
validator.addField<String>('contactNumber',
|
||||||
|
required: true, controller: TextEditingController());
|
||||||
|
validator.addField<String>('about', controller: TextEditingController());
|
||||||
|
validator.addField<String>('address',
|
||||||
|
required: true, controller: TextEditingController());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadIndustries() async {
|
||||||
|
final industries = await AuthService.getIndustries();
|
||||||
|
if (industries != null) {
|
||||||
|
setState(() {
|
||||||
|
_industries = industries;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
child: Form(
|
||||||
|
key: validator.formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 5,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Adventure starts here 🚀',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text("Make your app management easy and fun!"),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildTextField('organizationName', 'Organization Name'),
|
||||||
|
_buildTextField('email', 'Email',
|
||||||
|
keyboardType: TextInputType.emailAddress),
|
||||||
|
_buildTextField('contactPerson', 'Contact Person'),
|
||||||
|
_buildTextField('contactNumber', 'Contact Number',
|
||||||
|
keyboardType: TextInputType.phone),
|
||||||
|
_buildTextField('about', 'About Organization'),
|
||||||
|
_buildTextField('address', 'Current Address'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_buildPopupMenuField(
|
||||||
|
'Organization Size',
|
||||||
|
_sizes,
|
||||||
|
_selectedSize,
|
||||||
|
(val) => setState(() => _selectedSize = val),
|
||||||
|
'Please select organization size',
|
||||||
|
),
|
||||||
|
_buildPopupMenuField(
|
||||||
|
'Industry',
|
||||||
|
_industries.map((e) => e['name'] as String).toList(),
|
||||||
|
_selectedIndustryId != null
|
||||||
|
? _industries.firstWhere(
|
||||||
|
(e) => e['id'] == _selectedIndustryId)['name']
|
||||||
|
: null,
|
||||||
|
(val) {
|
||||||
|
setState(() {
|
||||||
|
final selectedIndustry = _industries.firstWhere(
|
||||||
|
(element) => element['name'] == val,
|
||||||
|
orElse: () => {});
|
||||||
|
_selectedIndustryId = selectedIndustry['id'];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'Please select industry',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _agreed,
|
||||||
|
onChanged: (val) => setState(() => _agreed = val ?? false),
|
||||||
|
fillColor: MaterialStateProperty.all(Colors.white),
|
||||||
|
checkColor: Colors.white,
|
||||||
|
side: MaterialStateBorderSide.resolveWith(
|
||||||
|
(states) =>
|
||||||
|
BorderSide(color: Colors.blueAccent, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Text('I agree to privacy policy & terms')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
onPressed: _submitForm,
|
||||||
|
child: const Text("Submit"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Back to login"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPopupMenuField(
|
||||||
|
String label,
|
||||||
|
List<String> items,
|
||||||
|
String? selectedValue,
|
||||||
|
ValueChanged<String?> onSelected,
|
||||||
|
String errorText,
|
||||||
|
) {
|
||||||
|
final bool hasError = selectedValue == null;
|
||||||
|
final GlobalKey _key = GlobalKey();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: FormField<String>(
|
||||||
|
validator: (value) => hasError ? errorText : null,
|
||||||
|
builder: (fieldState) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label,
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[700])),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
GestureDetector(
|
||||||
|
key: _key,
|
||||||
|
onTap: () async {
|
||||||
|
final RenderBox renderBox =
|
||||||
|
_key.currentContext!.findRenderObject() as RenderBox;
|
||||||
|
final Offset offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
final Size size = renderBox.size;
|
||||||
|
|
||||||
|
final selected = await showMenu<String>(
|
||||||
|
context: fieldState.context,
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
offset.dx,
|
||||||
|
offset.dy + size.height,
|
||||||
|
offset.dx + size.width,
|
||||||
|
offset.dy,
|
||||||
|
),
|
||||||
|
items: items
|
||||||
|
.map((item) => PopupMenuItem<String>(
|
||||||
|
value: item,
|
||||||
|
child: Text(item),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
if (selected != null) {
|
||||||
|
onSelected(selected);
|
||||||
|
fieldState.didChange(selected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: fieldState.errorText,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 14),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
selectedValue ?? 'Select $label',
|
||||||
|
style: TextStyle(
|
||||||
|
color: selectedValue == null ? Colors.grey : Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField(String fieldName, String label,
|
||||||
|
{TextInputType keyboardType = TextInputType.text}) {
|
||||||
|
final controller = validator.getController(fieldName);
|
||||||
|
final validatorFunc = validator.getValidation<String>(fieldName);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: validatorFunc,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submitForm() async {
|
||||||
|
bool isValid = validator.validateForm();
|
||||||
|
|
||||||
|
if (_selectedSize == null || _selectedIndustryId == null) {
|
||||||
|
isValid = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_agreed) {
|
||||||
|
isValid = false;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please agree to the privacy policy & terms')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
final formData = validator.getData();
|
||||||
|
|
||||||
|
final Map<String, dynamic> requestBody = {
|
||||||
|
'organizatioinName': formData['organizationName'],
|
||||||
|
'email': formData['email'],
|
||||||
|
'about': formData['about'],
|
||||||
|
'contactNumber': formData['contactNumber'],
|
||||||
|
'contactPerson': formData['contactPerson'],
|
||||||
|
'industryId': _selectedIndustryId ?? '',
|
||||||
|
'oragnizationSize': _sizeApiMap[_selectedSize] ?? '',
|
||||||
|
'terms': _agreed,
|
||||||
|
'address': formData['address'],
|
||||||
|
};
|
||||||
|
|
||||||
|
final error = await AuthService.requestDemo(requestBody);
|
||||||
|
|
||||||
|
if (error == null) {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: "Demo request submitted successfully!.",
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
showAppSnackbar(
|
||||||
|
title: "Success",
|
||||||
|
message: (error['error'] ?? 'Unknown error'),
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -80,7 +80,7 @@ class _LeftBarState extends State<LeftBar>
|
|||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: MySpacing.fromLTRB(0, 24, 0, 0),
|
padding: EdgeInsets.only(top: 50),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => Get.toNamed('/home'),
|
onTap: () => Get.toNamed('/home'),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
@ -106,7 +106,6 @@ class _LeftBarState extends State<LeftBar>
|
|||||||
physics: BouncingScrollPhysics(),
|
physics: BouncingScrollPhysics(),
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||||
children: [
|
children: [
|
||||||
Divider(),
|
|
||||||
labelWidget("Dashboard"),
|
labelWidget("Dashboard"),
|
||||||
NavigationItem(
|
NavigationItem(
|
||||||
iconData: LucideIcons.layout_dashboard,
|
iconData: LucideIcons.layout_dashboard,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user