login screen, choose country screen

This commit is contained in:
jayesh
2024-05-29 13:21:55 +05:30
parent 48444ddf11
commit 9c89e6c0d4
29 changed files with 429 additions and 73 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

View File

@@ -14,4 +14,7 @@ class RouteName {
//Welcome
static const String welcomeScreen = 'welcome';
//choose country
static const String chooseCountryScreen = 'chooseCountryScreen';
}

View File

@@ -2,6 +2,7 @@
import 'package:go_router/go_router.dart';
import 'package:tanami_app/core/routes/route_name.dart';
import 'package:tanami_app/features/countrySelection/presentation/pages/choose_country_screen.dart';
import 'package:tanami_app/features/welcome/presentation/pages/weclome_screen.dart';
import '../../features/login/presentation/pages/login_screen.dart';
@@ -43,6 +44,13 @@ final goRouter = GoRouter(
return const WelcomeScreen();
},
),
GoRoute(
name: RouteName.chooseCountryScreen,
path: RouteName.chooseCountryScreen,
builder: (context, state) {
return const ChooseCountryScreen();
},
),
]),
// GoRoute(

View File

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
class AppColor {
//Primary Color
static const Color primaryColor = Color(0xFF002F0F);
static const Color primaryColor2 = Color(0xFF004717);
//Welcome Color
static const Color indicatorActiveColor = Color(0xFF002F0F);
@@ -11,6 +12,7 @@ class AppColor {
//Common Color
static const Color plainWhite = Color(0xFFFFFFFF);
static const Color darkGreyColor = Color(0xFF343434);
static const Color plainBlack = Color(0xFF000000);
//Auth Color
static const Color charcoalColor = Color(0xFF272727);
@@ -28,4 +30,10 @@ class AppColor {
//Button Color
static const Color inactiveBtnColor = Color(0xFFD8D8D8);
static const Color inactiveBtnTxtColor = Color(0xFF8D8D8D);
//AppBar Color
static const Color appBarIconColor = Color(0xFF363636);
//Radio Color
static const Color radioActiveColor = Color(0xFF0B8933);
}

View File

@@ -25,14 +25,14 @@ class AppImages {
//Country Flag
static const String bahrainFlag =
"assets/images/country_flag/svg/bahrain_flag.svg";
"assets/images/country_flag/png/bahrain_flag.png";
static const String kuwaitFlag =
"assets/images/auth_screen/svg/kuwait_flag.svg";
static const String omanFlag = "assets/images/auth_screen/svg/oman_flag.svg";
"assets/images/country_flag/png/kuwait_flag.png";
static const String omanFlag = "assets/images/country_flag/png/oman_flag.png";
static const String qatarFlag =
"assets/images/country_flag/svg/qatar_flag.svg";
"assets/images/country_flag/png/qatar_flag.png";
static const String saudiArabiaflag =
"assets/images/auth_screen/svg/saudi_arabia_flag.svg";
"assets/images/country_flag/png/saudi_arabia_flag.png";
static const String unitedArabEmiratesFlag =
"assets/images/auth_screen/svg/united_arab_emirates_flag.svg";
"assets/images/country_flag/png/uae_flag.png";
}

View File

@@ -28,4 +28,14 @@ class AppText {
static const String enterPhoneNo = "Enter phone number";
static const String invalidPassword = "Invalid Password";
static const String forgorPassword = "Forgot Password";
//Country Name
static const String bahrainCountryText = "Bahrain";
static const String kuwaitCountryText = "Kuwait";
static const String omanCountryText = "Oman";
static const String qatarCountryText = "Qatar";
static const String saudiArabiaCountryText = "Saudi Arabia";
static const String uaeCountryText = "United Arab Emirates";
static const String confirmSelectionText = "Confirm selection";
}

View File

@@ -0,0 +1,20 @@
import 'package:tanami_app/core/styles/app_images.dart';
import 'package:tanami_app/core/styles/app_text.dart';
List<String> countryName = [
AppText.bahrainCountryText,
AppText.kuwaitCountryText,
AppText.omanCountryText,
AppText.qatarCountryText,
AppText.saudiArabiaCountryText,
AppText.uaeCountryText,
];
List<String> countryFlag = [
AppImages.bahrainFlag,
AppImages.kuwaitFlag,
AppImages.omanFlag,
AppImages.qatarFlag,
AppImages.saudiArabiaflag,
AppImages.unitedArabEmiratesFlag,
];

View File

@@ -0,0 +1,21 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'choose_country_event.dart';
import 'choose_country_state.dart';
class RadioBloc extends Bloc<RadioEvent, RadioState> {
RadioBloc() : super(RadioInitial()) {
on<RadioSelected>(_onRadioSelected);
}
void _onRadioSelected(RadioSelected event, Emitter<RadioState> emit) {
emit(RadioSelectionChanged(event.selectedIndex));
}
int get selectedCountry {
if (state is RadioSelectionChanged) {
return (state as RadioSelectionChanged).selectedIndex;
}
return 0;
}
}

View File

@@ -0,0 +1,17 @@
import 'package:equatable/equatable.dart';
abstract class RadioEvent extends Equatable {
const RadioEvent();
@override
List<Object> get props => [];
}
class RadioSelected extends RadioEvent {
final int selectedIndex;
const RadioSelected(this.selectedIndex);
@override
List<Object> get props => [selectedIndex];
}

View File

@@ -0,0 +1,19 @@
import 'package:equatable/equatable.dart';
abstract class RadioState extends Equatable {
const RadioState();
@override
List<Object> get props => [];
}
class RadioInitial extends RadioState {}
class RadioSelectionChanged extends RadioState {
final int selectedIndex;
const RadioSelectionChanged(this.selectedIndex);
@override
List<Object> get props => [selectedIndex];
}

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:tanami_app/core/routes/routes.dart';
import 'package:tanami_app/core/styles/app_text.dart';
import 'package:tanami_app/shared/components/appbar_widget.dart';
import '../../../../core/styles/app_color.dart';
import '../../../../shared/components/button_widget.dart';
import '../widgets/country_selection_list.dart';
class ChooseCountryLayout extends StatelessWidget {
const ChooseCountryLayout({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 35,
),
width: 1.sw,
height: 56.h,
child: ButtonWidget().elevatedBtn(
txtClr: AppColor.plainWhite,
function: () {
// radioBloc.add(const BackPressed(true));
goRouter.pop();
},
text: AppText.confirmSelectionText,
clr: AppColor.primaryColor2,
),
),
appBar: const AppBarWidget(
height: 75,
titleTxt: AppText.chooseCountry,
),
body: const CountrySelectionList(),
);
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'choose_country_layout.dart';
class ChooseCountryScreen extends StatelessWidget {
const ChooseCountryScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
resizeToAvoidBottomInset: true,
body: ChooseCountryLayout(),
);
}
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:tanami_app/core/styles/app_color.dart';
import 'package:tanami_app/core/utils/constant/country_flag_data.dart';
import 'package:tanami_app/shared/components/text_widget.dart';
import '../bloc/choose_country_bloc.dart';
import '../bloc/choose_country_event.dart';
import '../bloc/choose_country_state.dart';
class CountrySelectionList extends StatelessWidget {
const CountrySelectionList({super.key});
@override
Widget build(BuildContext context) {
final radioBloc = context.read<RadioBloc>();
return BlocBuilder<RadioBloc, RadioState>(
builder: (context, state) {
int selectedIndex = 0;
if (state is RadioSelectionChanged) {
selectedIndex = state.selectedIndex;
}
return Column(
children: List<Widget>.generate(countryFlag.length, (int index) {
return ListTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
countryFlag[index],
width: 24,
height: 24,
),
const Gap(10),
TextWidget().tex14W500(countryName[index],
clr: AppColor.charcoalColor),
],
),
leading: Radio<int>(
activeColor: AppColor.radioActiveColor,
value: index,
groupValue: selectedIndex,
onChanged: (int? value) {
if (value != null) {
radioBloc.add(RadioSelected(value));
}
},
),
);
}),
);
},
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'login_event.dart';
@@ -20,6 +18,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
phoneNumberTextField.addListener(_onFormFieldChanged);
passwordTextField.addListener(_onFormFieldChanged);
countrySelectionTextField.addListener(_onFormFieldChanged);
on<LoginFormChanged>(_onLoginFormChanged);
on<LoginSubmitted>((event, emit) async {
if (!formKey.currentState!.validate()) {
return;
@@ -43,19 +42,18 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
});
}
void _onFormFieldChanged() {
add(LoginFormChanged());
add(LoginFormChanged(
phoneNumberTextField.text,
passwordTextField.text,
countrySelectionTextField.text,
));
}
bool areFieldsFilled() {
return phoneNumberTextField.text.isNotEmpty &&
passwordTextField.text.isNotEmpty &&
countrySelectionTextField.text.isNotEmpty;
}
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginFormChanged) {
yield LoginFieldsState(areFieldsFilled());
}
void _onLoginFormChanged(LoginFormChanged event, Emitter<LoginState> emit) {
final areFieldsFilled = event.phoneNumber.isNotEmpty &&
event.password.isNotEmpty &&
event.country.isNotEmpty;
emit(LoginFieldsState(areFieldsFilled));
}
// Mock API function, replace with actual API call
@@ -71,6 +69,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
Future<void> close() {
phoneNumberTextField.dispose();
passwordTextField.dispose();
countrySelectionTextField.dispose();
return super.close();
}
}

View File

@@ -22,4 +22,13 @@ class LoginSubmitted extends LoginEvent {
List<Object> get props => [phoneNumber, password, countryResidence];
}
class LoginFormChanged extends LoginEvent {}
class LoginFormChanged extends LoginEvent {
final String phoneNumber;
final String password;
final String country;
const LoginFormChanged(this.phoneNumber, this.password, this.country);
@override
List<Object> get props => [phoneNumber, password, country];
}

View File

@@ -26,4 +26,7 @@ class LoginFieldsState extends LoginState {
final bool areFieldsFilled;
const LoginFieldsState(this.areFieldsFilled);
@override
List<Object> get props => [areFieldsFilled];
}

View File

@@ -5,17 +5,17 @@ import '../widgets/bottom_section.dart';
import '../widgets/top_section.dart';
class LoginLayout extends StatelessWidget {
LoginLayout({super.key});
const LoginLayout({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
//
children: [
const TopSection(),
children: const [
TopSection(),
LoginForm(),
const BottomSection(),
BottomSection(),
],
));
}

View File

@@ -21,9 +21,9 @@ class LoginScreen extends StatelessWidget {
),
BlocProvider(
create: (context) => PasswordVisibilityBloc(),
)
),
],
child: LoginLayout(),
child: const LoginLayout(),
),
);
}

View File

@@ -53,6 +53,8 @@ class BottomSection extends StatelessWidget {
bool isButtonEnabled = false;
if (state is LoginFieldsState) {
isButtonEnabled = state.areFieldsFilled;
} else if (state is LoginSuccess || state is LoginFailure) {
isButtonEnabled = true;
}
return Container(
margin: const EdgeInsets.symmetric(
@@ -61,7 +63,9 @@ class BottomSection extends StatelessWidget {
width: 1.sw,
height: 56.h,
child: ButtonWidget().elevatedBtn(
txtClr: AppColor.inactiveBtnTxtColor,
txtClr: isButtonEnabled
? AppColor.plainWhite
: AppColor.inactiveBtnTxtColor,
function: () {
isButtonEnabled
? context.read<LoginBloc>().add(
@@ -80,7 +84,7 @@ class BottomSection extends StatelessWidget {
},
text: AppText.loginText,
clr: isButtonEnabled
? AppColor.primaryColor
? AppColor.primaryColor2
: AppColor.inactiveBtnColor,
),
);

View File

@@ -2,52 +2,82 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:gap/gap.dart';
import 'package:tanami_app/core/styles/app_text.dart';
import 'package:tanami_app/core/utils/constant/country_flag_data.dart';
import '../../../../shared/components/form_label_textfield.dart';
import '../../../countrySelection/presentation/bloc/choose_country_bloc.dart';
import '../../../countrySelection/presentation/bloc/choose_country_state.dart';
import '../bloc/login_bloc.dart';
class LoginForm extends StatelessWidget {
LoginForm({super.key});
const LoginForm({super.key});
@override
Widget build(BuildContext context) {
final loginBloc = context.read<LoginBloc>();
return Form(
key: loginBloc.formKey,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
),
child: Align(
alignment: Alignment.topLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(50),
FormLabelTextField(
hintText: AppText.chooseCountry,
title: AppText.countryOfResidence,
type: "country selection",
textEditingController: loginBloc.countrySelectionTextField,
),
const Gap(20),
FormLabelTextField(
hintText: "+0 (000) 000 00 00",
title: AppText.phoneNumber,
type: "phone number",
textEditingController: loginBloc.phoneNumberTextField,
),
const Gap(20),
FormLabelTextField(
hintText: AppText.enterPassword,
title: AppText.password,
type: "password",
textEditingController: loginBloc.passwordTextField,
),
],
return BlocConsumer<RadioBloc, RadioState>(listener: (context, state) {
int selectedCountry = -1;
if (state is RadioSelectionChanged) {
selectedCountry = state.selectedIndex;
loginBloc.countrySelectionTextField.text = countryName[selectedCountry];
}
}, builder: (context, state) {
int selectedCountry = -1;
if (state is RadioSelectionChanged) {
selectedCountry = state.selectedIndex;
}
return Form(
key: loginBloc.formKey,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 14,
),
child: Align(
alignment: Alignment.topLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(50),
FormLabelTextField(
prefixWidget: selectedCountry == -1
? null
: Image.asset(
countryFlag[selectedCountry],
width: 20,
height: 20,
),
hintText: AppText.chooseCountry,
title: AppText.countryOfResidence,
type: "country selection",
textEditingController: loginBloc.countrySelectionTextField,
),
const Gap(20),
FormLabelTextField(
prefixWidget: selectedCountry == -1
? null
: Image.asset(
countryFlag[selectedCountry],
width: 20,
height: 20,
),
hintText: "+0 (000) 000 00 00",
title: AppText.phoneNumber,
type: "phone number",
textEditingController: loginBloc.phoneNumberTextField,
),
const Gap(20),
FormLabelTextField(
hintText: AppText.enterPassword,
title: AppText.password,
type: "password",
textEditingController: loginBloc.passwordTextField,
),
],
),
),
),
),
);
);
});
}
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/routes/routes.dart';
import 'core/utils/connectivity/network_connectivity.dart';
import 'features/countrySelection/presentation/bloc/choose_country_bloc.dart';
/* CREATED BY - JAYESH JAIN
DATE - 24-05-2024
@@ -47,16 +49,23 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
builder: (BuildContext context, Widget? child) => MaterialApp.router(
title: 'Tanami Capital',
theme: ThemeData(
useMaterial3: true,
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => RadioBloc(),
)
],
child: ScreenUtilInit(
builder: (BuildContext context, Widget? child) => MaterialApp.router(
title: 'Tanami Capital',
theme: ThemeData(
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
routerConfig: goRouter,
),
debugShowCheckedModeBanner: false,
routerConfig: goRouter,
designSize: const Size(390, 844),
),
designSize: const Size(390, 844),
);
}
}

View File

@@ -0,0 +1,73 @@
// ignore_for_file: non_constant_identifier_names, file_names, prefer_const_constructors
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:tanami_app/core/routes/routes.dart';
import 'package:tanami_app/core/styles/app_color.dart';
import 'package:tanami_app/shared/components/text_widget.dart';
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
@override
Size get preferredSize => Size.fromHeight(height!);
const AppBarWidget(
{super.key,
required this.titleTxt,
this.suffixIcon,
this.showLeading = true,
this.customBack,
this.backPageName = '',
this.customActionWidget,
this.onCustomActionPressed,
this.height = 105});
final String titleTxt;
final String? suffixIcon;
final bool? showLeading;
final bool? customBack;
final String? backPageName;
final Widget? customActionWidget;
final VoidCallback? onCustomActionPressed;
final double? height;
@override
Widget build(BuildContext context) {
return PreferredSize(
preferredSize: Size.fromHeight(height ?? 130),
child: AppBar(
scrolledUnderElevation: 0.0,
elevation: 0,
centerTitle: true,
title: TextWidget().tex20W700(titleTxt, clr: AppColor.charcoalColor),
leading: Padding(
padding: EdgeInsets.only(
left: 16.w,
),
child: GestureDetector(
onTap: () {
customBack ?? false
? goRouter.goNamed(backPageName!)
: goRouter.pop();
},
child: Padding(
padding: EdgeInsets.only(left: 8.w),
child: Icon(
Icons.arrow_back_rounded,
color: AppColor.appBarIconColor,
size: 25.r,
),
),
),
),
actions: [
if (customActionWidget != null)
InkWell(
onTap: onCustomActionPressed,
child: Padding(
padding: EdgeInsets.only(right: 14.w),
child: customActionWidget,
),
),
],
),
);
}
}

View File

@@ -5,6 +5,8 @@ import 'package:tanami_app/core/styles/app_text.dart';
import 'package:tanami_app/shared/components/password_text_form_field.dart';
import 'package:tanami_app/shared/components/text_widget.dart';
import '../../core/routes/route_name.dart';
import '../../core/routes/routes.dart';
import 'text_from_field_widget.dart';
class FormLabelTextField extends StatelessWidget {
@@ -14,11 +16,13 @@ class FormLabelTextField extends StatelessWidget {
required this.type,
required this.textEditingController,
required this.hintText,
this.prefixWidget,
});
final String title;
final String type;
final String hintText;
final TextEditingController textEditingController;
final Widget? prefixWidget;
@override
Widget build(BuildContext context) {
@@ -56,12 +60,18 @@ class FormLabelTextField extends StatelessWidget {
textEditingController: textEditingController,
readonly: type == "country selection" ? true : false,
hintText: hintText,
leadingIcon: prefixWidget,
suffixIcon: type == "country selection"
? const Icon(
Icons.arrow_forward_ios_rounded,
color: Color(0xFFB4B4B4),
)
: const SizedBox())
: const SizedBox(),
onTap: () {
if (type == "country selection") {
goRouter.pushNamed(RouteName.chooseCountryScreen);
}
})
],
);
}

View File

@@ -93,3 +93,4 @@ flutter:
- assets/images/auth_screen/png/
- assets/images/country_flag/
- assets/images/country_flag/svg/
- assets/images/country_flag/png/