feat: legacy mobile apps created
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import 'package:krow/core/application/clients/api/gql.dart';
|
||||
|
||||
const String getStaffPersonalInfoSchema = '''
|
||||
query GetPersonalInfo {
|
||||
me {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
middle_name
|
||||
email
|
||||
phone
|
||||
status
|
||||
avatar
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateStaffMutationSchema = '''
|
||||
$staffFragment
|
||||
mutation UpdateStaffPersonalInfo(\$input: UpdateStaffPersonalInfoInput!) {
|
||||
update_staff_personal_info(input: \$input) {
|
||||
...StaffFields
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateStaffMutationWithAvatarSchema = '''
|
||||
$staffFragment
|
||||
mutation UpdateStaffPersonalInfo(\$input: UpdateStaffPersonalInfoInput!, \$file: Upload!) {
|
||||
update_staff_personal_info(input: \$input) {
|
||||
...StaffFields
|
||||
}
|
||||
upload_staff_avatar(file: \$file)
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/staff/staff.dart';
|
||||
import 'package:krow/features/profile/personal_info/data/gql_schemas.dart';
|
||||
|
||||
@singleton
|
||||
class StaffPersonalInfoApiProvider {
|
||||
StaffPersonalInfoApiProvider(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Stream<Staff> getMeWithCache() async* {
|
||||
await for (var response in _client.queryWithCache(
|
||||
schema: getStaffPersonalInfoSchema,
|
||||
)) {
|
||||
if (response == null || response.data == null) continue;
|
||||
|
||||
if (response.hasException) {
|
||||
throw Exception(response.exception.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
final staffData = response.data?['me'] as Map<String, dynamic>? ?? {};
|
||||
if (staffData.isEmpty) continue;
|
||||
|
||||
yield Staff.fromJson(staffData);
|
||||
} catch (except) {
|
||||
log(
|
||||
'Exception in StaffApi on getMeWithCache()',
|
||||
error: except,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Staff> updateStaffPersonalInfo({
|
||||
required String firstName,
|
||||
required String? middleName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
String? avatarPath,
|
||||
}) async {
|
||||
MultipartFile? multipartFile;
|
||||
if (avatarPath != null) {
|
||||
var byteData = File(avatarPath).readAsBytesSync();
|
||||
|
||||
multipartFile = MultipartFile.fromBytes(
|
||||
'file',
|
||||
byteData,
|
||||
filename: '${DateTime.now().millisecondsSinceEpoch}.jpg',
|
||||
contentType: MediaType('image', 'jpg'),
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: multipartFile != null
|
||||
? updateStaffMutationWithAvatarSchema
|
||||
: updateStaffMutationSchema,
|
||||
body: {
|
||||
'input': {
|
||||
'first_name': firstName,
|
||||
'middle_name': middleName,
|
||||
'last_name': lastName,
|
||||
'email': email,
|
||||
},
|
||||
if (multipartFile != null) 'file': multipartFile,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
return Staff.fromJson(
|
||||
(result.data as Map<String, dynamic>)['update_staff_personal_info'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:krow/core/data/models/staff/staff.dart';
|
||||
|
||||
abstract class StaffPersonalInfoRepository {
|
||||
Stream<Staff> getPersonalInfo();
|
||||
|
||||
Future<Staff> updatePersonalInfo({
|
||||
required String firstName,
|
||||
required String? middleName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
String? avatarPath,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:krow/core/application/common/validators/email_validator.dart';
|
||||
import 'package:krow/core/application/di/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/staff.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/features/profile/email_verification/data/email_verification_service.dart';
|
||||
import 'package:krow/features/profile/personal_info/data/staff_repository.dart';
|
||||
|
||||
part 'personal_info_event.dart';
|
||||
|
||||
part 'personal_info_state.dart';
|
||||
|
||||
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState> {
|
||||
PersonalInfoBloc() : super(const PersonalInfoState()) {
|
||||
on<InitializeProfileInfoEvent>(_onInitializeProfileInfoEvent);
|
||||
|
||||
on<UpdateProfileImage>(_onUpdateProfileImage);
|
||||
|
||||
on<UpdateFirstName>(_onUpdateFirstName);
|
||||
|
||||
on<UpdateLastName>(_onUpdateLastName);
|
||||
|
||||
on<UpdateMiddleName>(_onUpdateMiddleName);
|
||||
|
||||
on<UpdateEmail>(_onUpdateEmail);
|
||||
|
||||
on<UpdatePhoneNumber>(_onUpdatePhoneNumber);
|
||||
|
||||
on<UpdateEmailVerificationStatus>(_onUpdateEmailVerification);
|
||||
|
||||
on<SaveProfileChanges>(_onSaveProfileChanges);
|
||||
}
|
||||
|
||||
Future<void> _onInitializeProfileInfoEvent(
|
||||
InitializeProfileInfoEvent event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isInEditMode: event.isInEditMode,
|
||||
status: event.isInEditMode ? StateStatus.loading : StateStatus.idle,
|
||||
),
|
||||
);
|
||||
if (!state.isInEditMode) return;
|
||||
|
||||
await for (final currentStaffData
|
||||
in getIt<StaffPersonalInfoRepository>().getPersonalInfo()) {
|
||||
try {
|
||||
emit(
|
||||
state.copyWith(
|
||||
firstName: currentStaffData.firstName,
|
||||
lastName: currentStaffData.lastName,
|
||||
middleName: currentStaffData.middleName,
|
||||
email: currentStaffData.email,
|
||||
phoneNumber: currentStaffData.phone,
|
||||
profileImageUrl: currentStaffData.avatar,
|
||||
isUpdateReceived: true,
|
||||
status: StateStatus.idle,
|
||||
),
|
||||
);
|
||||
} catch (except) {
|
||||
log(except.toString());
|
||||
} finally {
|
||||
emit(state.copyWith(isUpdateReceived: false));
|
||||
}
|
||||
}
|
||||
|
||||
if (state.status == StateStatus.loading) {
|
||||
emit(state.copyWith(status: StateStatus.idle));
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateProfileImage(
|
||||
UpdateProfileImage event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(state.copyWith(profileImagePath: event.imagePath));
|
||||
}
|
||||
|
||||
void _onUpdateFirstName(
|
||||
UpdateFirstName event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
firstName: event.firstName,
|
||||
validationErrors: Map.from(state.validationErrors)
|
||||
..remove(ProfileRequiredField.firstName),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateLastName(
|
||||
UpdateLastName event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
lastName: event.lastName,
|
||||
validationErrors: Map.from(state.validationErrors)
|
||||
..remove(ProfileRequiredField.lastName),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdateMiddleName(
|
||||
UpdateMiddleName event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(state.copyWith(middleName: event.middleName));
|
||||
}
|
||||
|
||||
void _onUpdateEmail(
|
||||
UpdateEmail event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
email: event.email,
|
||||
validationErrors: Map.from(state.validationErrors)
|
||||
..remove(ProfileRequiredField.email),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdatePhoneNumber(
|
||||
UpdatePhoneNumber event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(state.copyWith(phoneNumber: event.phoneNumber));
|
||||
}
|
||||
|
||||
void _onUpdateEmailVerification(
|
||||
UpdateEmailVerificationStatus event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isEmailVerified: event.isEmailVerified,
|
||||
shouldRouteToVerification: false,
|
||||
),
|
||||
);
|
||||
|
||||
if (state.isEmailVerified) {
|
||||
add(const SaveProfileChanges(shouldSkipEmailVerification: true));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSaveProfileChanges(
|
||||
SaveProfileChanges event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) async {
|
||||
final errorsMap = state.invalidate();
|
||||
if (errorsMap.isNotEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: StateStatus.error,
|
||||
validationErrors: errorsMap,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: StateStatus.loading, validationErrors: {}));
|
||||
|
||||
if (!event.shouldSkipEmailVerification) {
|
||||
log('Verifying email');
|
||||
bool isEmailVerified = getIt<EmailVerificationService>()
|
||||
.checkEmailForVerification(email: state.email);
|
||||
log('Email verified: $isEmailVerified');
|
||||
emit(
|
||||
state.copyWith(
|
||||
isEmailVerified: isEmailVerified,
|
||||
shouldRouteToVerification: !isEmailVerified,
|
||||
),
|
||||
);
|
||||
|
||||
log('Should route to Verification ${state.shouldRouteToVerification}');
|
||||
if (!isEmailVerified) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: StateStatus.idle,
|
||||
shouldRouteToVerification: false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
log('Continuing with profile saving');
|
||||
}
|
||||
|
||||
Staff? response;
|
||||
try {
|
||||
response = await getIt<StaffPersonalInfoRepository>().updatePersonalInfo(
|
||||
firstName: state.firstName,
|
||||
middleName: state.middleName,
|
||||
lastName: state.lastName,
|
||||
email: state.email,
|
||||
avatarPath: state.profileImagePath,
|
||||
);
|
||||
} catch (except) {
|
||||
log(
|
||||
'Error in PersonalInfoBloc. Event SaveProfileChanges',
|
||||
error: except,
|
||||
);
|
||||
} finally {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: response != null ? StateStatus.success : StateStatus.idle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
part of 'personal_info_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class PersonalInfoEvent {
|
||||
const PersonalInfoEvent();
|
||||
}
|
||||
|
||||
class InitializeProfileInfoEvent extends PersonalInfoEvent {
|
||||
const InitializeProfileInfoEvent(this.isInEditMode);
|
||||
|
||||
final bool isInEditMode;
|
||||
}
|
||||
|
||||
class UpdateProfileImage extends PersonalInfoEvent {
|
||||
const UpdateProfileImage(this.imagePath);
|
||||
|
||||
final String imagePath;
|
||||
}
|
||||
|
||||
class UpdateFirstName extends PersonalInfoEvent {
|
||||
const UpdateFirstName(this.firstName);
|
||||
|
||||
final String firstName;
|
||||
}
|
||||
|
||||
class UpdateLastName extends PersonalInfoEvent {
|
||||
const UpdateLastName(this.lastName);
|
||||
|
||||
final String lastName;
|
||||
}
|
||||
|
||||
class UpdateMiddleName extends PersonalInfoEvent {
|
||||
const UpdateMiddleName(this.middleName);
|
||||
|
||||
final String middleName;
|
||||
}
|
||||
|
||||
class UpdateEmail extends PersonalInfoEvent {
|
||||
const UpdateEmail(this.email);
|
||||
|
||||
final String email;
|
||||
}
|
||||
|
||||
class UpdatePhoneNumber extends PersonalInfoEvent {
|
||||
const UpdatePhoneNumber(this.phoneNumber);
|
||||
|
||||
final String phoneNumber;
|
||||
}
|
||||
|
||||
class UpdateEmailVerificationStatus extends PersonalInfoEvent {
|
||||
const UpdateEmailVerificationStatus({required this.isEmailVerified});
|
||||
|
||||
final bool isEmailVerified;
|
||||
}
|
||||
|
||||
class SaveProfileChanges extends PersonalInfoEvent {
|
||||
const SaveProfileChanges({this.shouldSkipEmailVerification = false});
|
||||
|
||||
final bool shouldSkipEmailVerification;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
part of 'personal_info_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class PersonalInfoState {
|
||||
const PersonalInfoState({
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.email = '',
|
||||
this.phoneNumber = '',
|
||||
this.middleName,
|
||||
this.profileImageUrl,
|
||||
this.profileImagePath,
|
||||
this.isInEditMode = true,
|
||||
this.isUpdateReceived = false,
|
||||
this.status = StateStatus.idle,
|
||||
this.validationErrors = const <ProfileRequiredField, String>{},
|
||||
this.isEmailVerified = false,
|
||||
this.shouldRouteToVerification = false,
|
||||
});
|
||||
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String phoneNumber;
|
||||
final String? middleName;
|
||||
final String? profileImageUrl;
|
||||
final String? profileImagePath;
|
||||
final bool isInEditMode;
|
||||
final bool isUpdateReceived;
|
||||
final StateStatus status;
|
||||
final Map<ProfileRequiredField, String> validationErrors;
|
||||
final bool isEmailVerified;
|
||||
final bool shouldRouteToVerification;
|
||||
|
||||
PersonalInfoState copyWith({
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? email,
|
||||
String? phoneNumber,
|
||||
String? middleName,
|
||||
String? profileImageUrl,
|
||||
String? profileImagePath,
|
||||
bool? isInEditMode,
|
||||
bool? isUpdateReceived,
|
||||
StateStatus? status,
|
||||
Map<ProfileRequiredField, String>? validationErrors,
|
||||
bool? isEmailVerified,
|
||||
bool? shouldRouteToVerification,
|
||||
}) {
|
||||
return PersonalInfoState(
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
email: email ?? this.email,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
middleName: middleName ?? this.middleName,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
|
||||
isInEditMode: isInEditMode ?? this.isInEditMode,
|
||||
isUpdateReceived: isUpdateReceived ?? this.isUpdateReceived,
|
||||
status: status ?? this.status,
|
||||
validationErrors: validationErrors ?? this.validationErrors,
|
||||
isEmailVerified: isEmailVerified ?? this.isEmailVerified,
|
||||
shouldRouteToVerification:
|
||||
shouldRouteToVerification ?? this.shouldRouteToVerification,
|
||||
);
|
||||
}
|
||||
|
||||
Map<ProfileRequiredField, String> invalidate() {
|
||||
final emailError = EmailValidator.validate(email, isRequired: true);
|
||||
|
||||
return {
|
||||
if (firstName.isEmpty) ProfileRequiredField.firstName: 'required_to_fill'.tr(),
|
||||
if (lastName.isEmpty) ProfileRequiredField.lastName: 'required_to_fill'.tr(),
|
||||
if (emailError != null) ProfileRequiredField.email: emailError,
|
||||
if (profileImagePath == null && profileImageUrl == null)
|
||||
ProfileRequiredField.avatar: 'required'.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ProfileRequiredField { firstName, lastName, email, avatar }
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/staff/staff.dart';
|
||||
import 'package:krow/features/profile/personal_info/data/staff_api_source.dart';
|
||||
import 'package:krow/features/profile/personal_info/data/staff_repository.dart';
|
||||
|
||||
@Singleton(as: StaffPersonalInfoRepository)
|
||||
class StaffPersonalInfoRepositoryImpl implements StaffPersonalInfoRepository {
|
||||
StaffPersonalInfoRepositoryImpl({
|
||||
required StaffPersonalInfoApiProvider staffApi,
|
||||
}) : _staffApi = staffApi;
|
||||
|
||||
final StaffPersonalInfoApiProvider _staffApi;
|
||||
|
||||
@override
|
||||
Stream<Staff> getPersonalInfo() {
|
||||
return _staffApi.getMeWithCache();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Staff> updatePersonalInfo({
|
||||
required String firstName,
|
||||
required String? middleName,
|
||||
required String lastName,
|
||||
required String email,
|
||||
String? avatarPath,
|
||||
}) {
|
||||
try {
|
||||
return _staffApi.updateStaffPersonalInfo(
|
||||
firstName: firstName,
|
||||
middleName: middleName,
|
||||
lastName: lastName,
|
||||
email: email,
|
||||
avatarPath: avatarPath,
|
||||
);
|
||||
} catch (exception) {
|
||||
log((exception as Error).stackTrace.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_loading_overlay.dart';
|
||||
import 'package:krow/features/profile/personal_info/domain/bloc/personal_info_bloc.dart';
|
||||
import 'package:krow/features/profile/personal_info/presentation/widgets/personal_info_form.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PersonalInfoScreen extends StatelessWidget implements AutoRouteWrapper {
|
||||
const PersonalInfoScreen({
|
||||
super.key,
|
||||
this.isInEditMode = false,
|
||||
});
|
||||
|
||||
final bool isInEditMode;
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
PersonalInfoBloc()..add(InitializeProfileInfoEvent(isInEditMode)),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
bool _handleBuildWhen(
|
||||
PersonalInfoState previous,
|
||||
PersonalInfoState current,
|
||||
) {
|
||||
return previous.status != current.status;
|
||||
}
|
||||
|
||||
bool _handleListenWhen(
|
||||
PersonalInfoState previous,
|
||||
PersonalInfoState current,
|
||||
) {
|
||||
return previous.status != current.status ||
|
||||
previous.shouldRouteToVerification != current.shouldRouteToVerification;
|
||||
}
|
||||
|
||||
Future<void> _handleListen(
|
||||
BuildContext context,
|
||||
PersonalInfoState state,
|
||||
) async {
|
||||
if (state.shouldRouteToVerification) {
|
||||
final isEmailVerified = await context.pushRoute<bool>(
|
||||
EmailVerificationRoute(
|
||||
verifiedEmail: state.email,
|
||||
userPhone: state.phoneNumber,
|
||||
),
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
context.read<PersonalInfoBloc>().add(
|
||||
UpdateEmailVerificationStatus(
|
||||
isEmailVerified: isEmailVerified ?? false,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.status == StateStatus.success && context.mounted) {
|
||||
if (isInEditMode) {
|
||||
context.maybePop();
|
||||
} else {
|
||||
context.pushRoute(
|
||||
EmergencyContactsRoute(isInEditMode: false),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Personal Information'.tr(),
|
||||
showNotification: isInEditMode,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
primary: false,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PersonalInfoForm(isInEditMode: isInEditMode),
|
||||
),
|
||||
bottomNavigationBar: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: _handleBuildWhen,
|
||||
listenWhen: _handleListenWhen,
|
||||
listener: _handleListen,
|
||||
builder: (context, state) {
|
||||
return KwLoadingOverlay(
|
||||
shouldShowLoading: state.status == StateStatus.loading,
|
||||
child: KwButton.primary(
|
||||
label: isInEditMode ? 'save_changes'.tr() : 'save_and_continue'.tr(),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<PersonalInfoBloc>()
|
||||
.add(const SaveProfileChanges());
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/profile_icon.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/features/profile/personal_info/domain/bloc/personal_info_bloc.dart';
|
||||
|
||||
class PersonalInfoForm extends StatefulWidget {
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
required this.isInEditMode,
|
||||
});
|
||||
|
||||
final bool isInEditMode;
|
||||
|
||||
@override
|
||||
State<PersonalInfoForm> createState() => _PersonalInfoFormState();
|
||||
}
|
||||
|
||||
class _PersonalInfoFormState extends State<PersonalInfoForm> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _middleNameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
late final _bloc = context.read<PersonalInfoBloc>();
|
||||
|
||||
void _syncControllersWithState(PersonalInfoState state) {
|
||||
_firstNameController.text = state.firstName;
|
||||
_lastNameController.text = state.lastName;
|
||||
_middleNameController.text = state.middleName ?? '';
|
||||
_emailController.text = state.email;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_syncControllersWithState(_bloc.state);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!widget.isInEditMode) ...[
|
||||
const Gap(4),
|
||||
Text(
|
||||
'lets_get_started'.tr(),
|
||||
style: AppTextStyles.headingH1,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'tell_us_about_yourself'.tr(),
|
||||
style: AppTextStyles.bodyMediumReg.copyWith(
|
||||
color: AppColors.blackGray,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.profileImageUrl != current.profileImageUrl ||
|
||||
previous.validationErrors != current.validationErrors,
|
||||
builder: (context, state) {
|
||||
return ProfileIcon(
|
||||
onChange: (imagePath) {
|
||||
_bloc.add(UpdateProfileImage(imagePath));
|
||||
},
|
||||
imagePath: state.profileImagePath,
|
||||
imageUrl: state.profileImageUrl,
|
||||
showError: state.validationErrors
|
||||
.containsKey(ProfileRequiredField.avatar),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: (previous, current) {
|
||||
return previous.status != current.status ||
|
||||
previous.validationErrors != current.validationErrors;
|
||||
},
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isUpdateReceived != current.isUpdateReceived,
|
||||
listener: (_, state) {
|
||||
if (!state.isUpdateReceived) return;
|
||||
|
||||
_syncControllersWithState(state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
KwTextInput(
|
||||
title: 'first_name'.tr(),
|
||||
hintText: 'enter_first_name'.tr(),
|
||||
controller: _firstNameController,
|
||||
keyboardType: TextInputType.name,
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
showError: state.validationErrors
|
||||
.containsKey(ProfileRequiredField.firstName),
|
||||
helperText:
|
||||
state.validationErrors[ProfileRequiredField.firstName],
|
||||
onChanged: (firstName) {
|
||||
_bloc.add(UpdateFirstName(firstName));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'last_name'.tr(),
|
||||
hintText: 'enter_last_name'.tr(),
|
||||
controller: _lastNameController,
|
||||
keyboardType: TextInputType.name,
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
showError: state.validationErrors
|
||||
.containsKey(ProfileRequiredField.lastName),
|
||||
helperText:
|
||||
state.validationErrors[ProfileRequiredField.lastName],
|
||||
onChanged: (lastName) {
|
||||
_bloc.add(UpdateLastName(lastName));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'middle_name_optional'.tr(),
|
||||
hintText: 'enter_middle_name'.tr(),
|
||||
controller: _middleNameController,
|
||||
keyboardType: TextInputType.name,
|
||||
textInputAction: TextInputAction.next,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: (middleName) {
|
||||
_bloc.add(UpdateMiddleName(middleName));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'email'.tr(),
|
||||
hintText: 'email@website.com',
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
showError: state.validationErrors
|
||||
.containsKey(ProfileRequiredField.email),
|
||||
helperText:
|
||||
state.validationErrors[ProfileRequiredField.email],
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (email) {
|
||||
_bloc.add(UpdateEmail(email));
|
||||
},
|
||||
),
|
||||
// It is nor clear if we will need it in future.
|
||||
// if (widget.isInEditMode) ...[
|
||||
// const Gap(8),
|
||||
// KwPhoneInput(
|
||||
// label: 'Phone',
|
||||
// onChanged: (phoneNumber) {
|
||||
// _bloc.add(UpdatePhoneNumber(phoneNumber));
|
||||
// },
|
||||
// ),
|
||||
// ]
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(40),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user