From ea15bd915222d1b29fe493ea699e05b0c931ba36 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Mon, 1 Sep 2025 17:01:38 +0300 Subject: [PATCH] bottom sheet --- .gitignore | 1 + assets/langs/ar-SA.json | 9 +- assets/langs/en-US.json | 7 +- lib/extensions/string_extensions.dart | 10 +- lib/generated/locale_keys.g.dart | 5 + lib/presentation/authantication/login.dart | 229 +++++++++++------- lib/theme/colors.dart | 26 +- .../bottomsheet/generic_bottom_sheet.dart | 145 +++++++++++ 8 files changed, 321 insertions(+), 111 deletions(-) create mode 100644 lib/widgets/bottomsheet/generic_bottom_sheet.dart diff --git a/.gitignore b/.gitignore index 79c113f..3ada50c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 8ff2eb4..361413e 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -776,5 +776,12 @@ "validPassportNumber": "يرجى إدخال رقم جواز سفر صالح", "continuePlan": "متابعة خطة العلاج؟", "aboutApp": "حول التطبيق", - "dontHaveAccount": "ليس لديك حساب؟" + "dontHaveAccount": "ليس لديك حساب؟", + "receiveOtpToast": "أين تود تلقي رمز التحقق OTP؟", + "enterPhoneNumber": "أدخل رقم الهاتف", + "enterEmailDesc": "أدخل عنوان بريدك الإلكتروني لإكمال عملية إنشاء ملف طبي", + "enterPhoneDesc": "أدخل رقم هاتفك لتلقي رمز التحقق ", + "pleaseChooseOption": "الرجاء اختيار من الخيارات أدناه لتلقي رمز التحقق OTP" + + } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 423c67b..a8c263c 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -772,5 +772,10 @@ "aboutApp": "About the app", "aboutPoints": "Online Appointment Booking & rescheduling, Insurance approval status, Find A doctor, Ask your doctor, Medical prescriptions, Lab results, Hospitals contact numbers, Doctor profiles, Hospitals locations, Pharmacies Locations, Hospital's Virtual Tour, Official Social Media, Vaccines Schedule, Health Calculators, Other Services", "termsConditions": "These Online Services Terms of Use (Service Terms) govern certain online services provided by Dr Sulaiman Al Habib Medical Services Group Company (HMG, we, us, our)...", - "dontHaveAccount": "Don't have an account?" + "dontHaveAccount": "Don't have an account?", + "receiveOtpToast": "Where would you like to receive OTP?", + "enterPhoneNumber": "Enter Phone Number", + "enterEmailDesc": "Enter your email address to complete the process of creating a medical file", + "enterPhoneDesc": "Enter your phone number to receive OTP verification code", + "pleaseChooseOption": "Please select from the below options to receive OTP" } \ No newline at end of file diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 2e74d25..2722901 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -198,31 +198,31 @@ extension EmailValidator on String { Widget toText20({Color? color, bool isBold = false}) => Text( this, - style: TextStyle(fontSize: 20.fSize, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: 0.64), + style: TextStyle(fontSize: 20.fSize, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: color ?? AppColors.blackColor, letterSpacing: -1), ); Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( this, maxLines: maxlines, - style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 21.fSize, letterSpacing: 0.64, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), + style: TextStyle(color: color ?? AppColors.blackColor, fontSize: 21.fSize, letterSpacing: -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); Widget toText22({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: 1, color: color ?? AppColors.blackColor, fontSize: 22.fSize, letterSpacing: 0.64, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle(height: 1, color: color ?? AppColors.blackColor, fontSize: 22.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText24({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.fSize, letterSpacing: 0.64, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle(height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText32({Color? color, bool isBold = false, bool isCenter = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, - style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.fSize, letterSpacing: 0.64, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), + style: TextStyle(height: 32 / 32, color: color ?? AppColors.blackColor, fontSize: 32.fSize, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); Widget toText44({Color? color, bool isBold = false}) => Text( diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index b2d7492..6b1ada7 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -775,5 +775,10 @@ abstract class LocaleKeys { static const continuePlan = 'continuePlan'; static const aboutApp = 'aboutApp'; static const dontHaveAccount = 'dontHaveAccount'; + static const receiveOtpToast = 'receiveOtpToast'; + static const enterPhoneNumber = 'enterPhoneNumber'; + static const enterEmailDesc = 'enterEmailDesc'; + static const enterPhoneDesc = 'enterPhoneDesc'; + static const pleaseChooseOption = 'pleaseChooseOption'; } diff --git a/lib/presentation/authantication/login.dart b/lib/presentation/authantication/login.dart index 150d446..84e1da3 100644 --- a/lib/presentation/authantication/login.dart +++ b/lib/presentation/authantication/login.dart @@ -3,16 +3,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/app_bar_widget.dart'; +import 'package:hmg_patient_app_new/widgets/bottomsheet/generic_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; -import 'package:hmg_patient_app_new/widgets/language_switcher.dart'; -import 'package:sizer/sizer.dart'; // Import sizer class LoginScreen extends StatefulWidget { @override @@ -32,98 +32,145 @@ class _LoginScreen extends State { @override Widget build(BuildContext context) { - return Sizer(// Wrap with Sizer - builder: (context, orientation, deviceType) { - return Scaffold( - appBar: CustomAppBar( - onBackPressed: () { - // Navigator.of(context).pop(); - }, - onLanguageChanged: (String value) { - print(value); - context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); - }, - ), - body: GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside - }, - child: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.only(left: 6.w, right: 6.w), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 45.w, height: 22.h, repeat: true, fit: BoxFit.cover), - SizedBox(height: 19.h), // Adjusted to sizer unit - Text( - LocaleKeys.welcomeToDrSulaiman.tr(), - style: context.dynamicTextStyle( - fontSize: 22.sp, - fontWeight: FontWeight.w600, - color: AppColors.textColor, - letterSpacing: -0.4, - height: 40 / 28, - ), - ), - SizedBox(height: 4.h), // Adjusted to sizer unit (approx 32px) - TextInputWidget( - labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}", - hintText: "xxxxxxxxx", - controller: TextEditingController(), - keyboardType: TextInputType.number, - isEnable: true, - prefix: null, - autoFocus: true, - isBorderAllowed: false, - isAllowLeadingIcon: true, - padding: EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.w), - leadingIcon: AppAssets.student_card, - errorMessage: "Please enter a valid national ID or file number", - hasError: true, - ), - SizedBox(height: 2.h), // Adjusted to sizer unit (approx 16px) - CustomButton( - text: LocaleKeys.login.tr(), - icon: AppAssets.login1, - iconColor: Colors.white, - onPressed: () {}, - ), - SizedBox(height: 1.8.h), // Adjusted to sizer unit (approx 14px) - Center( - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: context.dynamicTextStyle( - color: Colors.black, - fontSize: 14.sp, // Adjusted to sizer unit - height: 26 / 16, // This height is a ratio, may need re-evaluation - fontWeight: FontWeight.w500, - ), - children: [ - TextSpan(text: LocaleKeys.dontHaveAccount.tr(), style: context.dynamicTextStyle()), - TextSpan(text: " "), - TextSpan( - text: LocaleKeys.registernow.tr(), - style: context.dynamicTextStyle( - color: AppColors.primaryRedColor, - fontSize: 14.sp, // Adjusted to sizer unit - height: 26 / 16, // Ratio - fontWeight: FontWeight.w500, - ), - recognizer: TapGestureRecognizer()..onTap = () {}, - ), - ], + return Scaffold( + appBar: CustomAppBar( + onBackPressed: () { + // Navigator.of(context).pop(); + }, + onLanguageChanged: (String value) { + print(value); + context.setLocale(value == 'en' ? Locale('ar', 'SA') : Locale('en', 'US')); + }, + ), + body: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); // Dismiss the keyboard when tapping outside + }, + child: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.only(left: 6.h, right: 6.h), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Utils.showLottie(context: context, assetPath: AppAnimations.login, width: 45.h, height: 22.h, repeat: true, fit: BoxFit.cover), + SizedBox(height: 19.h), // Adjusted to sizer unit + LocaleKeys.welcomeToDrSulaiman.tr().toText24(), + SizedBox(height: 4.h), // Adjusted to sizer unit (approx 32px) + TextInputWidget( + labelText: "${LocaleKeys.nationalId.tr()} / ${LocaleKeys.fileNo.tr()}", + hintText: "xxxxxxxxx", + controller: TextEditingController(), + keyboardType: TextInputType.number, + isEnable: true, + prefix: null, + autoFocus: true, + isBorderAllowed: false, + isAllowLeadingIcon: true, + padding: EdgeInsets.symmetric(vertical: 1.h, horizontal: 2.h), + leadingIcon: AppAssets.student_card, + errorMessage: "Please enter a valid national ID or file number", + hasError: true, + ), + SizedBox(height: 2.h), // Adjusted to sizer unit (approx 16px) + CustomButton( + text: LocaleKeys.login.tr(), + icon: AppAssets.login1, + iconColor: Colors.white, + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + useSafeArea: true, + backgroundColor: Colors.transparent, + enableDrag: false, + builder: (bottomSheetContext) => StatefulBuilder( + builder: (BuildContext context, StateSetter setModalState) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom), + child: SingleChildScrollView( + child: GenericBottomSheet( + countryCode: "selectedCountry.countryCode", + initialPhoneNumber: "", + textController: TextEditingController(), + isEnableCountryDropdown: true, + onCountryChange: (value) {}, + onChange: (String? value) {}, + buttons: [ + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: CustomButton( + text: "TranslationBase.of(context).sendOTPSMS", + onPressed: () {}, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedBorderColor, + textColor: AppColors.whiteColor, + icon: AppAssets.message, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: "LocaleKeys.oR.tr()".toText16(), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 10, top: 10), + child: CustomButton( + text: "LocaleKeys.sendOTPWHATSAPP.tr(),", + onPressed: () {}, + backgroundColor: Colors.white, + borderColor: AppColors.borderOnlyColor, + textColor: AppColors.textColor, + icon: AppAssets.whatsapp, + ), + ), + ], + ), + )); + }, ), - ).withVerticalPadding(2.h), // Adjusted to sizer unit - ), - ], - ), + ); + }, + ), + SizedBox(height: 1.8.h), // Adjusted to sizer unit (approx 14px) + Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: context.dynamicTextStyle( + color: Colors.black, + fontSize: 14.fSize, // Adjusted to sizer unit + height: 26 / 16, // This height is a ratio, may need re-evaluation + fontWeight: FontWeight.w500, + ), + children: [ + TextSpan(text: LocaleKeys.dontHaveAccount.tr(), style: context.dynamicTextStyle()), + TextSpan(text: " "), + TextSpan( + text: LocaleKeys.registernow.tr(), + style: context.dynamicTextStyle( + color: AppColors.primaryRedColor, + fontSize: 14.fSize, // Adjusted to sizer unit + height: 26 / 16, // Ratio + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer()..onTap = () {}, + ), + ], + ), + ).withVerticalPadding(2.h), // Adjusted to sizer unit + ), + ], ), ), ), - ); - }); + ), + ); } } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 4987ffe..d20c5f2 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -8,7 +8,6 @@ class AppColors { static const buttonColor = Color(0xFF6A46F5); static const splashBgColor = Color(0xFF3C355D); - static const blackColor = Color(0xFF000000); static const lightGray = Color(0xFFF4F5F7); static const lightPurple = Color(0xFFB7A3E6); static const scaffoldBgColor = Color(0xFFF8F8F8); @@ -28,19 +27,20 @@ class AppColors { static const Color bgGreenColor = Color(0xFF18C273); static const Color textColor = Color(0xFF2E3039); static const Color borderOnlyColor = Color(0xFF2E3039); + static const blackColor = textColor; //Chips -static const Color successColor = Color(0xff18C273); -static const Color errorColor = Color(0xFFED1C2B); -static const Color alertColor = Color(0xFFD48D05); -static const Color infoColor = Color(0xFF0B85F7); -static const Color warningColor = Color(0xFFFFCC00); -static const Color greyColor = Color(0xFFEFEFF0); + static const Color successColor = Color(0xff18C273); + static const Color errorColor = Color(0xFFED1C2B); + static const Color alertColor = Color(0xFFD48D05); + static const Color infoColor = Color(0xFF0B85F7); + static const Color warningColor = Color(0xFFFFCC00); + static const Color greyColor = Color(0xFFEFEFF0); -static const Color successLightColor = Color(0xFF18C27326); -static const Color errorLightColor = Color(0xFFED1C2B1A); -static const Color alertLightColor = Color(0xFFD48D0526); -static const Color infoLightColor = Color(0xFF0B85F726); -static const Color warningLightColor = Color(0xFFFFCC0026); -static const Color greyLightColor = Color(0xFFEFEFF026); + static const Color successLightColor = Color(0xFF18C27326); + static const Color errorLightColor = Color(0xFFED1C2B1A); + static const Color alertLightColor = Color(0xFFD48D0526); + static const Color infoLightColor = Color(0xFF0B85F726); + static const Color warningLightColor = Color(0xFFFFCC0026); + static const Color greyLightColor = Color(0xFFEFEFF026); } diff --git a/lib/widgets/bottomsheet/generic_bottom_sheet.dart b/lib/widgets/bottomsheet/generic_bottom_sheet.dart new file mode 100644 index 0000000..13a02a5 --- /dev/null +++ b/lib/widgets/bottomsheet/generic_bottom_sheet.dart @@ -0,0 +1,145 @@ +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; + +class GenericBottomSheet extends StatefulWidget { + String? countryCode; + String? initialPhoneNumber; + final List buttons; + TextEditingController? textController; + final bool isForEmail; + Function(Country)? onCountryChange; + final bool isEnableCountryDropdown; + final bool isFromSavedLogin; + Function(String?)? onChange; + // FocusNode myFocusNode; + + GenericBottomSheet( + {this.countryCode = "", + this.initialPhoneNumber = "", + required this.buttons, + this.textController, + this.isForEmail = false, + this.onCountryChange, + this.isEnableCountryDropdown = false, + this.isFromSavedLogin = false, + this.onChange, + // required this.myFocusNode + }); + + @override + _GenericBottomSheetState createState() => _GenericBottomSheetState(); +} + +class _GenericBottomSheetState extends State { + @override + void initState() { + super.initState(); + + if (!widget.isForEmail) { + widget.textController = TextEditingController(text: widget.initialPhoneNumber); + } + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + bottom: Platform.isIOS ? false : true, + child: GestureDetector( + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Directionality( + textDirection: Directionality.of(context), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Color(0xFFF8F8FA), + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Title + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: widget.isFromSavedLogin + ? LocaleKeys.receiveOtpToast.tr().toText24() + : widget.isForEmail + ? LocaleKeys.enterEmail.tr().toText24() + : LocaleKeys.enterPhoneNumber.tr().toText24()), + InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Padding( + padding: const EdgeInsets.only(top: 10), + child: Utils.buildSvgWithAssets( + icon: AppAssets.cross_circle, + ), + )), + ], + ), + const SizedBox(height: 8), + // Subtitle + widget.isFromSavedLogin + ? LocaleKeys.pleaseChooseOption.tr().toText16() + : widget.isForEmail + ? LocaleKeys.enterEmailDesc.tr().toText16() + : LocaleKeys.enterPhoneDesc.tr().toText16(), + + if (widget.isFromSavedLogin) + ...[] + else ...[ + widget.textController != null + ? TextInputWidget( + labelText: widget.isForEmail ? LocaleKeys.email : LocaleKeys.phoneNumber, + hintText: widget.isForEmail ? "demo@gmail.com" : "5xxxxxxxx", + controller: widget.textController!, + padding: EdgeInsets.only(top: 8, bottom: 8, left: 8, right: 8), + keyboardType: widget.isForEmail ? TextInputType.emailAddress : TextInputType.number, + onChange: (value) { + widget.textController!.text = value!; + if (widget.onChange != null) { + widget.onChange!(value); + } + }, + isEnable: true, + // focusNode: widget.myFocusNode, + isReadOnly: widget.isFromSavedLogin, + prefix: widget.isForEmail ? null : widget.countryCode, + isBorderAllowed: false, + isAllowLeadingIcon: true, + isCountryDropDown: widget.isEnableCountryDropdown, + leadingIcon: widget.isForEmail ? AppAssets.email : AppAssets.smart_phone, + ) + : SizedBox(), + ], + + SizedBox(height: 24), + ...widget.buttons, + ], + ), + ), + ), + ), + ); + } +}