diff --git a/assets/pngs/forgot_password.png b/assets/pngs/forgot_password.png new file mode 100644 index 0000000..5e9c728 Binary files /dev/null and b/assets/pngs/forgot_password.png differ diff --git a/lib/core/constants/assets.dart b/lib/core/constants/assets.dart index 9ca138f..e3b1316 100644 --- a/lib/core/constants/assets.dart +++ b/lib/core/constants/assets.dart @@ -22,6 +22,9 @@ class AssetsManager { static const passwordVisibilityOff = 'assets/svgs/password_visibility_off.svg'; + //forgot password page + static const forgotPasswordImage = 'assets/pngs/forgot_password.png'; + // tenders page static const tenderLogo = 'assets/icons/tenderLogo.png'; static const like = 'assets/icons/like.svg'; diff --git a/lib/core/constants/strings.dart b/lib/core/constants/strings.dart index abbff66..a215fd8 100644 --- a/lib/core/constants/strings.dart +++ b/lib/core/constants/strings.dart @@ -25,6 +25,14 @@ class AppStrings { static const passwordLabel = 'Password'; static const loginButton = 'Login'; + //forgot password + static const String forgotPasswordTitle = 'Forgot Password'; + static const String forgotPasswordSubtitle = + 'Enter your registered email and company ID and submit your request.'; + static const String emailAddressHint = 'Email Address'; + static const String backToSignInButton = 'Back to Sign In'; + static const String resetLink = 'Reset Link'; + //Home static const String home = 'Home'; static const String partnership = 'Partnership'; diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index ad01d6c..101dda3 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -4,6 +4,7 @@ import 'package:tm_app/views/detail/pages/detail_screen.dart'; import 'package:tm_app/views/home/pages/home_screen.dart'; import 'package:tm_app/views/login/pages/login_screen.dart'; +import '../../views/forgot_password/pages/forgot_password_screen.dart'; import '../../views/profile/pages/profile_screen.dart'; import '../../views/tenders/pages/tenders_screen.dart'; import '../../views/your_tenders/pages/your_tenders_screen.dart'; @@ -157,3 +158,14 @@ class TenderDetailRouteData extends GoRouteData with _$TenderDetailRouteData { return TenderDetailScreen(tenderId: tenderId); } } + +@TypedGoRoute(path: '/forgot-password') +class ForgotPasswordRouteData extends GoRouteData + with _$ForgotPasswordRouteData { + const ForgotPasswordRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return ForgotPasswordScreen(); + } +} diff --git a/lib/core/routes/app_routes.g.dart b/lib/core/routes/app_routes.g.dart index 708bf42..aa78fd8 100644 --- a/lib/core/routes/app_routes.g.dart +++ b/lib/core/routes/app_routes.g.dart @@ -11,6 +11,7 @@ List get $appRoutes => [ $appShellRouteData, $yourTendersRouteData, $tenderDetailRouteData, + $forgotPasswordRouteData, ]; RouteBase get $loginScreenRoute => @@ -198,3 +199,30 @@ mixin _$TenderDetailRouteData on GoRouteData { @override void replace(BuildContext context) => context.replace(location); } + +RouteBase get $forgotPasswordRouteData => GoRouteData.$route( + path: '/forgot-password', + + factory: _$ForgotPasswordRouteData._fromState, +); + +mixin _$ForgotPasswordRouteData on GoRouteData { + static ForgotPasswordRouteData _fromState(GoRouterState state) => + const ForgotPasswordRouteData(); + + @override + String get location => GoRouteData.$location('/forgot-password'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart index 230abd3..c78f850 100644 --- a/lib/gen/assets.gen.dart +++ b/lib/gen/assets.gen.dart @@ -17,6 +17,9 @@ class $AssetsIconsGen { /// File path: assets/icons/SE.png AssetGenImage get se => const AssetGenImage('assets/icons/SE.png'); + /// File path: assets/icons/approve.svg + String get approve => 'assets/icons/approve.svg'; + /// File path: assets/icons/arrow-circle-left.svg String get arrowCircleLeft => 'assets/icons/arrow-circle-left.svg'; @@ -59,6 +62,12 @@ class $AssetsIconsGen { /// File path: assets/icons/location.svg String get location => 'assets/icons/location.svg'; + /// File path: assets/icons/logo_big.svg + String get logoBig => 'assets/icons/logo_big.svg'; + + /// File path: assets/icons/logo_small.svg + String get logoSmall => 'assets/icons/logo_small.svg'; + /// File path: assets/icons/menu.svg String get menu => 'assets/icons/menu.svg'; @@ -96,6 +105,9 @@ class $AssetsIconsGen { /// File path: assets/icons/thumb.svg String get thumb => 'assets/icons/thumb.svg'; + /// File path: assets/icons/thumb_like.svg + String get thumbLike => 'assets/icons/thumb_like.svg'; + /// File path: assets/icons/tick-circle.svg String get tickCircle => 'assets/icons/tick-circle.svg'; @@ -105,6 +117,7 @@ class $AssetsIconsGen { /// List of all assets List get values => [ se, + approve, arrowCircleLeft, arrowCircleRight, arrowLeftSmall, @@ -119,6 +132,8 @@ class $AssetsIconsGen { homeActive, like, location, + logoBig, + logoSmall, menu, moon, notification, @@ -131,6 +146,7 @@ class $AssetsIconsGen { tenderLogoPng, tenderLogoSvg, thumb, + thumbLike, tickCircle, tick, ]; diff --git a/lib/view_models/auth_view_model.dart b/lib/view_models/auth_view_model.dart index 02c5641..fde0344 100644 --- a/lib/view_models/auth_view_model.dart +++ b/lib/view_models/auth_view_model.dart @@ -19,8 +19,10 @@ class AuthViewModel with ChangeNotifier { final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); final FocusNode usernameFocus = FocusNode(); final FocusNode passwordFocus = FocusNode(); + final FocusNode emailFocus = FocusNode(); bool _obscurePassword = true; bool get obscurePassword => _obscurePassword; diff --git a/lib/views/forgot_password/pages/d_forgot_apssword_page.dart b/lib/views/forgot_password/pages/d_forgot_apssword_page.dart new file mode 100644 index 0000000..e76cdc5 --- /dev/null +++ b/lib/views/forgot_password/pages/d_forgot_apssword_page.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tm_app/core/utils/size_config.dart'; + +import '../../../core/constants/assets.dart'; +import '../../../core/constants/strings.dart'; +import '../../../core/theme/colors.dart'; +import '../../../view_models/auth_view_model.dart'; +import '../../login/widgets/login_textfield.dart'; +import '../../shared/base_button.dart'; + +class DesktopForgotPasswordPage extends StatelessWidget { + const DesktopForgotPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.backgroundColor, + body: Center( + child: Consumer( + builder: (context, viewModel, _) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 120), + child: SizedBox( + width: 364, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0.w()), + child: SvgPicture.asset( + AssetsManager.logoBig, + width: double.infinity, + height: 50.0.h(), + ), + ), + SizedBox(height: 72.0.h()), + Text( + AppStrings.forgotPasswordTitle, + style: TextStyle( + fontSize: 28.0.sp(), + fontWeight: FontWeight.bold, + color: AppColors.grey80, + ), + ), + SizedBox(height: 16.0.h()), + Text( + AppStrings.forgotPasswordSubtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + SizedBox(height: 40.0.h()), + LoginTextField( + controller: viewModel.emailController, + focusNode: viewModel.emailFocus, + label: AppStrings.emailAddressHint, + isPassword: false, + ), + SizedBox(height: 70.0.h()), + BaseButton( + isEnabled: false, + text: AppStrings.resetLink, + onPressed: () async { + if (viewModel.isLoginEnabled) { + await viewModel.login(); + } + }, + ), + SizedBox(height: 12), + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + AppStrings.backToSignInButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), + ], + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric(vertical: 20.0.h()), + child: ClipRRect( + borderRadius: BorderRadius.circular(32.0), + child: Image.asset( + AssetsManager.forgotPasswordImage, + width: 796.0, + height: 944.0, + fit: BoxFit.fitWidth, + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/views/forgot_password/pages/forgot_password_screen.dart b/lib/views/forgot_password/pages/forgot_password_screen.dart new file mode 100644 index 0000000..617ad98 --- /dev/null +++ b/lib/views/forgot_password/pages/forgot_password_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +import '../../shared/responsive_builder.dart'; +import 'd_forgot_apssword_page.dart'; +import 'm_forgot_password_page.dart'; +import 't_forgot_password_page.dart'; + +class ForgotPasswordScreen extends StatelessWidget { + const ForgotPasswordScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ResponsiveBuilder( + mobile: MobileForgotPasswordPage(), + tablet: TabletForgotPasswordPage(), + desktop: DesktopForgotPasswordPage(), + ); + } +} diff --git a/lib/views/forgot_password/pages/m_forgot_password_page.dart b/lib/views/forgot_password/pages/m_forgot_password_page.dart new file mode 100644 index 0000000..101d239 --- /dev/null +++ b/lib/views/forgot_password/pages/m_forgot_password_page.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tm_app/core/utils/size_config.dart'; + +import '../../../core/constants/assets.dart'; +import '../../../core/constants/strings.dart'; +import '../../../core/theme/colors.dart'; +import '../../../view_models/auth_view_model.dart'; +import '../../login/widgets/login_textfield.dart'; +import '../../shared/base_button.dart'; + +class MobileForgotPasswordPage extends StatelessWidget { + const MobileForgotPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.backgroundColor, + body: Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: 24.0.w()), + child: Consumer( + builder: (context, viewModel, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0.w()), + child: SvgPicture.asset( + AssetsManager.logoBig, + width: double.infinity, + height: 50.0.h(), + ), + ), + SizedBox(height: 72.0.h()), + Text( + AppStrings.forgotPasswordTitle, + style: TextStyle( + fontSize: 28.0.sp(), + fontWeight: FontWeight.bold, + color: AppColors.grey80, + ), + ), + SizedBox(height: 16.0.h()), + Text( + AppStrings.forgotPasswordSubtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + SizedBox(height: 40.0.h()), + LoginTextField( + controller: viewModel.emailController, + focusNode: viewModel.emailFocus, + label: AppStrings.emailAddressHint, + isPassword: false, + ), + SizedBox(height: 70.0.h()), + BaseButton( + isEnabled: false, + text: AppStrings.resetLink, + onPressed: () async { + if (viewModel.isLoginEnabled) { + await viewModel.login(); + } + }, + ), + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + AppStrings.backToSignInButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), + ], + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/views/forgot_password/pages/t_forgot_password_page.dart b/lib/views/forgot_password/pages/t_forgot_password_page.dart new file mode 100644 index 0000000..83cc1f4 --- /dev/null +++ b/lib/views/forgot_password/pages/t_forgot_password_page.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tm_app/core/utils/size_config.dart'; + +import '../../../core/constants/assets.dart'; +import '../../../core/constants/strings.dart'; +import '../../../core/theme/colors.dart'; +import '../../../view_models/auth_view_model.dart'; +import '../../login/widgets/login_textfield.dart'; +import '../../shared/base_button.dart'; + +class TabletForgotPasswordPage extends StatelessWidget { + const TabletForgotPasswordPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Consumer( + builder: (context, viewModel, _) { + return Center( + child: SizedBox( + width: 364, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 22.0.w()), + child: SvgPicture.asset( + AssetsManager.logoBig, + width: double.infinity, + height: 50.0.h(), + ), + ), + SizedBox(height: 72.0.h()), + Text( + AppStrings.forgotPasswordTitle, + style: TextStyle( + fontSize: 28.0.sp(), + fontWeight: FontWeight.bold, + color: AppColors.grey80, + ), + ), + SizedBox(height: 16.0.h()), + Text( + AppStrings.forgotPasswordSubtitle, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w400, + color: AppColors.grey60, + ), + ), + SizedBox(height: 40.0.h()), + LoginTextField( + controller: viewModel.emailController, + focusNode: viewModel.emailFocus, + label: AppStrings.emailAddressHint, + isPassword: false, + ), + SizedBox(height: 70.0.h()), + BaseButton( + isEnabled: false, + text: AppStrings.resetLink, + onPressed: () async { + if (viewModel.isLoginEnabled) { + await viewModel.login(); + } + }, + ), + SizedBox(height: 12), + TextButton( + onPressed: () { + context.pop(); + }, + child: Text( + AppStrings.backToSignInButton, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/views/login/pages/login_desktop_page.dart b/lib/views/login/pages/login_desktop_page.dart index fb31680..af88a3f 100644 --- a/lib/views/login/pages/login_desktop_page.dart +++ b/lib/views/login/pages/login_desktop_page.dart @@ -7,6 +7,7 @@ import 'package:tm_app/core/theme/colors.dart'; import '../../../core/constants/assets.dart'; import '../../../core/constants/strings.dart'; +import '../../../core/routes/app_routes.dart'; import '../../../core/utils/size_config.dart'; import '../../../view_models/auth_view_model.dart'; import '../widgets/widgets.dart'; @@ -46,7 +47,6 @@ class _LoginDesktopPageState extends State { @override Widget build(BuildContext context) { - SizeConfig.init(context); return Scaffold( backgroundColor: AppColors.backgroundColor, body: Center( @@ -55,7 +55,7 @@ class _LoginDesktopPageState extends State { kIsWeb ? MainAxisAlignment.spaceAround : MainAxisAlignment.center, children: [ SizedBox( - width: 526, + width: 536, child: Consumer( builder: (context, viewModel, _) { return Column( @@ -108,6 +108,20 @@ class _LoginDesktopPageState extends State { } }, ), + SizedBox(height: 32.0), + TextButton( + onPressed: () { + ForgotPasswordRouteData().push(context); + }, + child: Text( + AppStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), ], ); }, diff --git a/lib/views/login/pages/login_mobile_page.dart b/lib/views/login/pages/login_mobile_page.dart index 0981c1d..43c354e 100644 --- a/lib/views/login/pages/login_mobile_page.dart +++ b/lib/views/login/pages/login_mobile_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; +import 'package:tm_app/core/routes/app_routes.dart'; import 'package:tm_app/core/theme/colors.dart'; import '../../../core/constants/assets.dart'; @@ -103,6 +104,20 @@ class _LoginMobilePageState extends State { } }, ), + SizedBox(height: 32.0.h()), + TextButton( + onPressed: () { + ForgotPasswordRouteData().push(context); + }, + child: Text( + AppStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), ], ); }, diff --git a/lib/views/login/pages/login_tablet_page.dart b/lib/views/login/pages/login_tablet_page.dart index ca37a9d..1047e57 100644 --- a/lib/views/login/pages/login_tablet_page.dart +++ b/lib/views/login/pages/login_tablet_page.dart @@ -5,6 +5,7 @@ import 'package:tm_app/core/theme/colors.dart'; import '../../../core/constants/assets.dart'; import '../../../core/constants/strings.dart'; +import '../../../core/routes/app_routes.dart'; import '../../../core/utils/size_config.dart'; import '../../../view_models/auth_view_model.dart'; import '../widgets/widgets.dart'; @@ -45,7 +46,6 @@ class _LoginTabletPageState extends State { @override Widget build(BuildContext context) { - SizeConfig.init(context); return Scaffold( backgroundColor: AppColors.backgroundColor, body: Center( @@ -101,6 +101,20 @@ class _LoginTabletPageState extends State { } : null, ), + SizedBox(height: 32.0), + TextButton( + onPressed: () { + ForgotPasswordRouteData().push(context); + }, + child: Text( + AppStrings.forgotPassword, + style: TextStyle( + fontSize: 14.0.sp(), + fontWeight: FontWeight.w500, + color: AppColors.mainBlue, + ), + ), + ), ], ); }, diff --git a/lib/views/login/widgets/login_textfield.dart b/lib/views/login/widgets/login_textfield.dart index d0dd050..b2351a1 100644 --- a/lib/views/login/widgets/login_textfield.dart +++ b/lib/views/login/widgets/login_textfield.dart @@ -9,7 +9,7 @@ class LoginTextField extends StatelessWidget { final TextEditingController controller; final FocusNode focusNode; final String label; - final String iconPath; + final String? iconPath; final bool isPassword; final bool obscureText; final double? borderRedus; @@ -21,7 +21,7 @@ class LoginTextField extends StatelessWidget { required this.controller, required this.focusNode, required this.label, - required this.iconPath, + this.iconPath, super.key, this.isPassword = false, this.obscureText = false, @@ -45,18 +45,23 @@ class LoginTextField extends StatelessWidget { horizontal: 12.0.w(), ), labelText: label, - prefixIcon: Padding( - padding: EdgeInsets.all(prefixIconPadding ?? 12.0.w()), - child: SvgPicture.asset( - iconPath, - width: 24.0.w(), - height: 24.0.h(), - colorFilter: ColorFilter.mode( - focusNode.hasFocus ? AppColors.grey80 : AppColors.grey60, - BlendMode.srcIn, - ), - ), - ), + prefixIcon: + iconPath != null + ? Padding( + padding: EdgeInsets.all(prefixIconPadding ?? 12.0.w()), + child: SvgPicture.asset( + iconPath!, + width: 24.0.w(), + height: 24.0.h(), + colorFilter: ColorFilter.mode( + focusNode.hasFocus + ? AppColors.grey80 + : AppColors.grey60, + BlendMode.srcIn, + ), + ), + ) + : null, suffixIcon: isPassword ? IconButton(