feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
|
||||
const String getPersonalInfoSchema = '''
|
||||
query GetPersonalInfo {
|
||||
client_profile {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateStaffMutationSchema = '''
|
||||
mutation UpdateStaffPersonalInfo(\$input: UpdateClientProfileInput!) {
|
||||
update_client_profile(input: \$input) {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
''';
|
||||
|
||||
const String updateStaffMutationWithAvatarSchema = '''
|
||||
mutation UpdateStaffPersonalInfo(\$input: UpdateClientProfileInput!, \$file: Upload!) {
|
||||
update_client_profile(input: \$input) {
|
||||
id
|
||||
first_name
|
||||
last_name
|
||||
title
|
||||
avatar
|
||||
auth_info {
|
||||
email
|
||||
phone
|
||||
}
|
||||
}
|
||||
upload_client_avatar(file: \$file)
|
||||
}
|
||||
''';
|
||||
|
||||
const deactivateClientProfileSchema = '''
|
||||
mutation deactivate_client_profile {
|
||||
deactivate_client_profile(){
|
||||
id
|
||||
}
|
||||
}
|
||||
''';
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:graphql_flutter/graphql_flutter.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/application/clients/api/api_client.dart';
|
||||
import 'package:krow/core/data/models/client/client.dart';
|
||||
import 'package:krow/features/profile/data/gql_schemas.dart';
|
||||
|
||||
@singleton
|
||||
class ClientProfileApiProvider {
|
||||
ClientProfileApiProvider(this._client);
|
||||
|
||||
final ApiClient _client;
|
||||
|
||||
Stream<ClientModel> getMeWithCache() async* {
|
||||
await for (var response in _client.queryWithCache(
|
||||
schema: getPersonalInfoSchema,
|
||||
)) {
|
||||
if (response == null) continue;
|
||||
|
||||
if (response.hasException) {
|
||||
throw Exception(response.exception.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
final profileData =
|
||||
response.data?['client_profile'] as Map<String, dynamic>? ?? {};
|
||||
if (profileData.isEmpty) continue;
|
||||
|
||||
yield ClientModel.fromJson(profileData);
|
||||
} catch (except) {
|
||||
log(
|
||||
'Exception in StaffApi on getMeWithCache()',
|
||||
error: except,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<ClientModel> updatePersonalInfo({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String phone,
|
||||
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.trim(),
|
||||
'last_name': lastName.trim(),
|
||||
'phone': phone.trim(),
|
||||
},
|
||||
if (multipartFile != null) 'file': multipartFile,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
|
||||
return ClientModel.fromJson(
|
||||
(result.data as Map<String, dynamic>)['update_client_profile'],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deactivateClientProfile() async {
|
||||
final QueryResult result = await _client.mutate(
|
||||
schema: deactivateClientProfileSchema,
|
||||
);
|
||||
|
||||
if (result.hasException) {
|
||||
throw Exception(result.exception.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:krow/core/data/models/client/client.dart';
|
||||
|
||||
abstract class ClientProfileRepository {
|
||||
Stream<ClientModel> getPersonalInfo();
|
||||
|
||||
Future<ClientModel> updatePersonalInfo({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String phone,
|
||||
String? avatarPath,
|
||||
});
|
||||
|
||||
Future<void> deactivateProfile();
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import 'dart:developer';
|
||||
|
||||
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/enums/state_status.dart';
|
||||
import 'package:krow/core/data/models/client/client.dart';
|
||||
import 'package:krow/features/profile/data/profile_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<UpdateEmail>(_onUpdateEmail);
|
||||
|
||||
on<UpdatePhoneNumber>(_onUpdatePhoneNumber);
|
||||
|
||||
on<SaveProfileChanges>(_onSaveProfileChanges);
|
||||
|
||||
on<DeactivateProfile>(_onDeactivateProfile);
|
||||
}
|
||||
|
||||
Future<void> _onInitializeProfileInfoEvent(
|
||||
InitializeProfileInfoEvent event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: StateStatus.idle,
|
||||
),
|
||||
);
|
||||
|
||||
await for (final currentProfileData
|
||||
in getIt<ClientProfileRepository>().getPersonalInfo()) {
|
||||
try {
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
firstName: currentProfileData.firstName,
|
||||
lastName: currentProfileData.lastName,
|
||||
email: currentProfileData.authInfo?.email,
|
||||
profileImageUrl: currentProfileData.avatar,
|
||||
phoneNumber: currentProfileData.authInfo?.phone,
|
||||
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));
|
||||
}
|
||||
|
||||
void _onUpdateLastName(
|
||||
UpdateLastName event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(state.copyWith(lastName: event.lastName));
|
||||
}
|
||||
|
||||
void _onUpdateEmail(
|
||||
UpdateEmail event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
final error = EmailValidator.validate(event.email);
|
||||
emit(
|
||||
state.copyWith(
|
||||
email: event.email,
|
||||
emailValidationError: error ?? '',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onUpdatePhoneNumber(
|
||||
UpdatePhoneNumber event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) {
|
||||
emit(state.copyWith(phoneNumber: event.phoneNumber));
|
||||
}
|
||||
|
||||
Future<void> _onSaveProfileChanges(
|
||||
SaveProfileChanges event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) async {
|
||||
if (!state.isValid) {
|
||||
emit(state.copyWith(status: StateStatus.error));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
|
||||
ClientModel? response;
|
||||
try {
|
||||
response = await getIt<ClientProfileRepository>().updatePersonalInfo(
|
||||
firstName: state.firstName,
|
||||
lastName: state.lastName,
|
||||
phone: state.phoneNumber,
|
||||
avatarPath: state.profileImagePath,
|
||||
);
|
||||
} finally {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: response != null ? StateStatus.success : StateStatus.idle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeactivateProfile(
|
||||
DeactivateProfile event,
|
||||
Emitter<PersonalInfoState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: StateStatus.loading));
|
||||
|
||||
try {
|
||||
await getIt<ClientProfileRepository>().deactivateProfile();
|
||||
} finally {
|
||||
emit(state.copyWith(status: StateStatus.idle, deleted: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
part of 'personal_info_bloc.dart';
|
||||
|
||||
@immutable
|
||||
sealed class PersonalInfoEvent {}
|
||||
|
||||
class InitializeProfileInfoEvent extends PersonalInfoEvent {
|
||||
InitializeProfileInfoEvent();
|
||||
}
|
||||
|
||||
class UpdateProfileImage extends PersonalInfoEvent {
|
||||
UpdateProfileImage(this.imagePath);
|
||||
|
||||
final String imagePath;
|
||||
}
|
||||
|
||||
class UpdateFirstName extends PersonalInfoEvent {
|
||||
UpdateFirstName(this.firstName);
|
||||
|
||||
final String firstName;
|
||||
}
|
||||
|
||||
class UpdateLastName extends PersonalInfoEvent {
|
||||
UpdateLastName(this.lastName);
|
||||
|
||||
final String lastName;
|
||||
}
|
||||
|
||||
class UpdateEmail extends PersonalInfoEvent {
|
||||
UpdateEmail(this.email);
|
||||
|
||||
final String email;
|
||||
}
|
||||
|
||||
class UpdatePhoneNumber extends PersonalInfoEvent {
|
||||
UpdatePhoneNumber(this.phoneNumber);
|
||||
|
||||
final String phoneNumber;
|
||||
}
|
||||
|
||||
class SaveProfileChanges extends PersonalInfoEvent {}
|
||||
|
||||
class DeactivateProfile extends PersonalInfoEvent {}
|
||||
@@ -0,0 +1,71 @@
|
||||
part of 'personal_info_bloc.dart';
|
||||
|
||||
@immutable
|
||||
class PersonalInfoState {
|
||||
const PersonalInfoState({
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.email = '',
|
||||
this.phoneNumber = '',
|
||||
this.profileImageUrl,
|
||||
this.profileImagePath,
|
||||
this.isUpdateReceived = false,
|
||||
this.status = StateStatus.idle,
|
||||
this.emailValidationError = '',
|
||||
this.deleted = false
|
||||
});
|
||||
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String email;
|
||||
final String phoneNumber;
|
||||
final String? profileImageUrl;
|
||||
final String? profileImagePath;
|
||||
final bool isUpdateReceived;
|
||||
final StateStatus status;
|
||||
final String emailValidationError;
|
||||
final bool deleted;
|
||||
|
||||
PersonalInfoState copyWith({
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
String? email,
|
||||
String? phoneNumber,
|
||||
String? profileImageUrl,
|
||||
String? profileImagePath,
|
||||
bool? isUpdateReceived,
|
||||
StateStatus? status,
|
||||
String? emailValidationError,
|
||||
bool? deleted,
|
||||
}) {
|
||||
return PersonalInfoState(
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
email: email ?? this.email,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||
profileImageUrl: profileImageUrl ?? this.profileImageUrl,
|
||||
isUpdateReceived: isUpdateReceived ?? this.isUpdateReceived,
|
||||
status: status ?? this.status,
|
||||
deleted: deleted ?? this.deleted,
|
||||
emailValidationError: emailValidationError ?? this.emailValidationError,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isFilled {
|
||||
return firstName.isNotEmpty && lastName.isNotEmpty && email.isNotEmpty;
|
||||
// Phone should not be validated on initial profile setup
|
||||
//(!isInEditMode || phoneNumber.isNotEmpty) &&
|
||||
//photo is optional
|
||||
// (profileImagePath != null || profileImageUrl != null);
|
||||
}
|
||||
|
||||
bool get isValid {
|
||||
return firstName.isNotEmpty &&
|
||||
lastName.isNotEmpty &&
|
||||
emailValidationError.isEmpty &&
|
||||
// Phone should not be validated on initial profile setup
|
||||
//(!isInEditMode || phoneNumber.isNotEmpty) &&
|
||||
(profileImagePath != null || profileImageUrl != null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:krow/core/data/models/client/client.dart';
|
||||
import 'package:krow/features/profile/data/profile_api_source.dart';
|
||||
import 'package:krow/features/profile/data/profile_repository.dart';
|
||||
|
||||
@Singleton(as: ClientProfileRepository)
|
||||
class ClientProfileRepositoryImpl implements ClientProfileRepository {
|
||||
ClientProfileRepositoryImpl({
|
||||
required ClientProfileApiProvider apiProvider,
|
||||
}) : _apiProvider = apiProvider;
|
||||
|
||||
final ClientProfileApiProvider _apiProvider;
|
||||
|
||||
@override
|
||||
Stream<ClientModel> getPersonalInfo() {
|
||||
return _apiProvider.getMeWithCache();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ClientModel> updatePersonalInfo({
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
required String phone,
|
||||
String? avatarPath,
|
||||
}) {
|
||||
try {
|
||||
return _apiProvider.updatePersonalInfo(
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
phone: phone,
|
||||
avatarPath: avatarPath,
|
||||
);
|
||||
} catch (exception) {
|
||||
log((exception as Error).stackTrace.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deactivateProfile() {
|
||||
return _apiProvider.deactivateClientProfile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.dart';
|
||||
import 'package:krow/core/presentation/styles/theme.dart';
|
||||
import 'package:krow/core/presentation/widgets/scroll_layout_helper.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.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/domain/bloc/personal_info_bloc.dart';
|
||||
import 'package:krow/features/profile/presentation/widgets/personal_info_form.dart';
|
||||
|
||||
@RoutePage()
|
||||
class PersonalInfoScreen extends StatelessWidget {
|
||||
const PersonalInfoScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: KwAppBar(
|
||||
titleText: 'Edit Profile',
|
||||
),
|
||||
body: ScrollLayoutHelper(
|
||||
upperWidget: const PersonalInfoForm(),
|
||||
lowerWidget: BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.status != current.status ||
|
||||
previous.isFilled != current.isFilled,
|
||||
listenWhen: (previous, current) => previous.status != current.status,
|
||||
listener: (context, state) {
|
||||
if (state.status == StateStatus.success) {}
|
||||
|
||||
if(state.deleted){
|
||||
FirebaseAuth.instance.signOut();
|
||||
context.router.replace(const SplashRoute());
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
KwLoadingOverlay(
|
||||
shouldShowLoading: state.status == StateStatus.loading,
|
||||
child: KwButton.primary(
|
||||
label: 'Save edits',
|
||||
height: 52,
|
||||
disabled: !state.isFilled,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<PersonalInfoBloc>()
|
||||
.add(SaveProfileChanges());
|
||||
},
|
||||
),
|
||||
),
|
||||
Gap(12),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Delete Account', onPressed: () {
|
||||
_showDeleteAccDialog(context);
|
||||
})
|
||||
.copyWith(color: AppColors.statusError)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteAccDialog(BuildContext context) {
|
||||
KwDialog.show(
|
||||
context: context,
|
||||
icon: Assets.images.icons.alertTriangle,
|
||||
state: KwDialogState.negative,
|
||||
title: 'Are you sure you want to delete account?',
|
||||
message:
|
||||
'Deleting your account is permanent and cannot be undone. You will lose all your data, including saved preferences, history, and any content associated with your account',
|
||||
primaryButtonLabel: 'Delete Account',
|
||||
secondaryButtonLabel: 'Cancel',
|
||||
onPrimaryButtonPressed: (dialogContext) {
|
||||
Navigator.pop(dialogContext);
|
||||
BlocProvider.of<PersonalInfoBloc>(context)
|
||||
.add(DeactivateProfile());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/application/routing/routes.gr.dart';
|
||||
import 'package:krow/core/presentation/gen/assets.gen.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/ui_kit/kw_app_bar.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_button.dart';
|
||||
import 'package:krow/features/profile/domain/bloc/personal_info_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ProfilePreviewScreen extends StatelessWidget {
|
||||
const ProfilePreviewScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: AppColors.primaryBlue,
|
||||
child: Assets.images.bgPattern.svg(
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: KwAppBar(
|
||||
contentColor: AppColors.grayWhite,
|
||||
titleText: 'Profile',
|
||||
centerTitle: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.bgProfileCard,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildUserPhoto(state.profileImageUrl, '${state.firstName} ${state.lastName}'),
|
||||
const Gap(16),
|
||||
_buildUserInfo(context, state),
|
||||
const Gap(24),
|
||||
KwButton.accent(
|
||||
label: 'Edit Profile',
|
||||
onPressed: () {
|
||||
context.router.push(const PersonalInfoRoute());
|
||||
}),
|
||||
const Gap(12),
|
||||
KwButton.outlinedPrimary(
|
||||
label: 'Log Out',
|
||||
onPressed: ()async {
|
||||
await FirebaseAuth.instance.signOut();
|
||||
context.router.replace(const SplashRoute());
|
||||
}).copyWith(color: AppColors.tintDarkRed),
|
||||
const Gap(12),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Container _buildUserPhoto(String? imageUrl, String? userName) {
|
||||
return Container(
|
||||
width: 174,
|
||||
height: 174,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.darkBgActiveButtonState,
|
||||
),
|
||||
child: imageUrl == null || imageUrl.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
getInitials(userName),
|
||||
style: AppTextStyles.headingH1.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ClipOval(
|
||||
child: Image.network(
|
||||
imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
width: 174,
|
||||
height: 174,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Column _buildUserInfo(BuildContext context, PersonalInfoState state) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${state.firstName} ${state.lastName}',
|
||||
style: AppTextStyles.headingH3.copyWith(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (state.email.isNotEmpty) ...[
|
||||
const Gap(8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.userProfile.sms.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.tintDropDownButton,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
state.email,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.tintDropDownButton),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
if (state.phoneNumber.isNotEmpty) ...[
|
||||
const Gap(8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Assets.images.icons.userProfile.call.svg(
|
||||
colorFilter: const ColorFilter.mode(
|
||||
AppColors.tintDropDownButton,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
state.phoneNumber,
|
||||
style: AppTextStyles.bodyMediumReg
|
||||
.copyWith(color: AppColors.tintDropDownButton),
|
||||
)
|
||||
],
|
||||
),
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String getInitials(String? name) {
|
||||
if (name == null || name.isEmpty) return '';
|
||||
if(name.trim().isEmpty) {
|
||||
return '';
|
||||
}
|
||||
List<String> nameParts = name.split(' ');
|
||||
if(nameParts.first.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
if (nameParts.length == 1) {
|
||||
return nameParts[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
return (nameParts[0][0] + nameParts[1][0]).toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:krow/core/data/enums/state_status.dart';
|
||||
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
|
||||
import 'package:krow/core/presentation/widgets/profile_icon.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_input.dart';
|
||||
import 'package:krow/core/presentation/widgets/ui_kit/kw_phone_input.dart';
|
||||
import 'package:krow/features/profile/domain/bloc/personal_info_bloc.dart';
|
||||
|
||||
class PersonalInfoForm extends StatefulWidget {
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PersonalInfoForm> createState() => _PersonalInfoFormState();
|
||||
}
|
||||
|
||||
class _PersonalInfoFormState extends State<PersonalInfoForm> {
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
// final _emailController = TextEditingController();
|
||||
late final _bloc = context.read<PersonalInfoBloc>();
|
||||
|
||||
void _syncControllersWithState(PersonalInfoState state) {
|
||||
_firstNameController.text = state.firstName;
|
||||
_lastNameController.text = state.lastName;
|
||||
// _emailController.text = state.email;
|
||||
_phoneController.text = state.phoneNumber;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_syncControllersWithState(_bloc.state);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Gap(16),
|
||||
BlocBuilder<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.profileImageUrl != current.profileImageUrl,
|
||||
builder: (context, state) {
|
||||
return ProfileIcon(
|
||||
diameter: 96,
|
||||
onChange: (imagePath) {
|
||||
_bloc.add(UpdateProfileImage(imagePath));
|
||||
},
|
||||
imagePath: state.profileImagePath,
|
||||
imageUrl: state.profileImageUrl,
|
||||
imageQuality: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
BlocConsumer<PersonalInfoBloc, PersonalInfoState>(
|
||||
buildWhen: (previous, current) => previous.status != current.status,
|
||||
listenWhen: (previous, current) =>
|
||||
previous.isUpdateReceived != current.isUpdateReceived,
|
||||
listener: (_, state) {
|
||||
if (!state.isUpdateReceived) return;
|
||||
|
||||
_syncControllersWithState(state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
final bool isValidationFailed = state.status == StateStatus.error;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Name', style: AppTextStyles.headingH3),
|
||||
const Gap(12),
|
||||
KwTextInput(
|
||||
title: 'First Name',
|
||||
hintText: 'Enter your first name',
|
||||
controller: _firstNameController,
|
||||
keyboardType: TextInputType.name,
|
||||
onChanged: (firstName) {
|
||||
_bloc.add(UpdateFirstName(firstName));
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
KwTextInput(
|
||||
title: 'Last Name',
|
||||
hintText: 'Enter your last name',
|
||||
controller: _lastNameController,
|
||||
keyboardType: TextInputType.name,
|
||||
onChanged: (lastName) {
|
||||
_bloc.add(UpdateLastName(lastName));
|
||||
},
|
||||
),
|
||||
// const Gap(8),
|
||||
// KwTextInput(
|
||||
// title: 'Middle Name (optional)',
|
||||
// hintText: 'Enter your middle name',
|
||||
// controller: _middleNameController,
|
||||
// keyboardType: TextInputType.name,
|
||||
// onChanged: (middleName) {
|
||||
// _bloc.add(UpdateMiddleName(middleName));
|
||||
// },
|
||||
// ),
|
||||
const Gap(24),
|
||||
const Text('Phone Number', style: AppTextStyles.headingH3),
|
||||
const Gap(12),
|
||||
KwPhoneInput(
|
||||
controller: _phoneController,
|
||||
onChanged: (phoneNumber) {
|
||||
_bloc.add(UpdatePhoneNumber(phoneNumber));
|
||||
},
|
||||
),
|
||||
|
||||
// const Gap(24),
|
||||
// const Text('Email', style: AppTextStyles.headingH3),
|
||||
// const Gap(12),
|
||||
// KwTextInput(
|
||||
// hintText: 'email@website.com',
|
||||
// controller: _emailController,
|
||||
// keyboardType: TextInputType.emailAddress,
|
||||
// showError: isValidationFailed &&
|
||||
// state.emailValidationError.isNotEmpty,
|
||||
// helperText: state.emailValidationError,
|
||||
// onChanged: (email) {
|
||||
// _bloc.add(UpdateEmail(email));
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(40),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow/features/profile/domain/bloc/personal_info_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ProfileFlowScreen extends StatelessWidget {
|
||||
const ProfileFlowScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (BuildContext context) =>
|
||||
PersonalInfoBloc()..add(InitializeProfileInfoEvent()),
|
||||
child: const AutoRouter(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user