Skip to content

Commit cf7956f

Browse files
committed
feat: complete update profile bloc
Signed-off-by: Aman <aman2@me.iitr.ac.in>
1 parent 040dad1 commit cf7956f

11 files changed

Lines changed: 247 additions & 81 deletions

File tree

lib/domain/models/handle.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ part 'handle.g.dart';
66
@freezed
77
class Handle with _$Handle {
88
factory Handle({
9-
@JsonKey(name: 'handle.codechef') String? codechef,
10-
@JsonKey(name: 'handle.codeforces') String? codeforces,
11-
@JsonKey(name: 'handle.hackerrank') String? hackerrank,
12-
@JsonKey(name: 'handle.spoj') String? spoj,
13-
@JsonKey(name: 'handle.leetcode') String? leetcode,
9+
@JsonKey(name: 'codechef') String? codechef,
10+
@JsonKey(name: 'codeforces') String? codeforces,
11+
@JsonKey(name: 'hackerrank') String? hackerrank,
12+
@JsonKey(name: 'spoj') String? spoj,
13+
@JsonKey(name: 'leetcode') String? leetcode,
1414
}) = _Handle;
1515

1616
factory Handle.fromJson(Map<String, dynamic> json) => _$HandleFromJson(json);

lib/domain/repositories/user_repository.dart

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ class UserRepository {
193193
);
194194

195195
if (response['status_code'] == 200) {
196+
if (response['data'] == 'null') return <Following>[];
197+
196198
return List<Following>.from(
197199
response['data'].map((e) => Following.fromJson(e)),
198200
);
@@ -204,9 +206,7 @@ class UserRepository {
204206
static Future<bool> verifyHandle(String site, String handle) async {
205207
final endpoint = 'user/verify/$site?handle=$handle';
206208

207-
final response = await ApiService.post(
208-
endpoint,
209-
);
209+
final response = await ApiService.get(endpoint);
210210

211211
return response['status_code'] == 200;
212212
}
@@ -279,6 +279,8 @@ class UserRepository {
279279
);
280280

281281
if (response['status_code'] == 200) {
282+
if (response['data'] == 'null') return <ActivityDetails>[];
283+
282284
return List<ActivityDetails>.from(
283285
response['data'].map((e) => ActivityDetails.fromJson(e)),
284286
);
@@ -298,7 +300,7 @@ class UserRepository {
298300
final response = await ApiService.post(
299301
endpoint,
300302
headers: headers,
301-
data: <String, String>{
303+
data: {
302304
'new_password': newPassword,
303305
'old_password': oldPassword,
304306
},
@@ -319,7 +321,7 @@ class UserRepository {
319321
final headers = <String, dynamic>{};
320322

321323
ApiService.addTokenToHeaders(headers);
322-
final response = await ApiService.post(
324+
final response = await ApiService.put(
323325
endpoint,
324326
headers: headers,
325327
data: data,

lib/presentation/profile/widgets/profile_header.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import 'package:get/get.dart';
77
import '../../../data/constants/assets.dart';
88
import '../../../data/constants/colors.dart';
99
import '../../../data/constants/routes.dart';
10+
import '../../../data/constants/strings.dart';
1011
import '../../../data/constants/styles.dart';
12+
import '../../../domain/repositories/user_repository.dart';
13+
import '../../../utils/snackbar.dart';
1114
import '../bloc/profile_bloc.dart';
1215
import '../components/follow_button.dart';
1316
import '../components/following_button.dart';
@@ -85,7 +88,15 @@ class ProfileHeader extends StatelessWidget {
8588
horizontal: 12.w,
8689
vertical: 4.h,
8790
),
88-
onTap: () {},
91+
onTap: () async {
92+
final res = await UserRepository.logout();
93+
if (res) {
94+
Get.offNamed(AppRoutes.login);
95+
return;
96+
}
97+
98+
showSnackBar(message: AppStrings.genericError);
99+
},
89100
child: Text(
90101
'Logout',
91102
style: AppStyles.h6.copyWith(

lib/presentation/update_profile/bloc/update_profile_bloc.dart

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import 'package:equatable/equatable.dart';
55
import 'package:flutter/widgets.dart';
66
import 'package:flutter_bloc/flutter_bloc.dart';
77
import 'package:freezed_annotation/freezed_annotation.dart';
8+
import 'package:get/get.dart';
89

10+
import '../../../data/constants/strings.dart';
911
import '../../../data/services/local/image_service.dart';
1012
import '../../../data/services/local/storage_service.dart';
1113
import '../../../domain/models/status.dart';
@@ -27,6 +29,7 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
2729
on<UpdateFocusField>(_onUpdateFocusField);
2830
on<ToggleObscure>(_onToggleObscure);
2931
on<UpdatePassword>(_onUpdatePassword);
32+
on<UpdateUserDetails>(_onUpdateUserDetails);
3033
}
3134

3235
static List<String> institutes = <String>[
@@ -37,27 +40,43 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
3740
'Indian Institute of Technology Bombay',
3841
];
3942

43+
static User get user => StorageService.user!;
44+
45+
static Map<String, TextEditingController?> controllers =
46+
<String, TextEditingController?>{
47+
'name': TextEditingController(),
48+
'username': TextEditingController(),
49+
'codechef': TextEditingController(),
50+
'codeforces': TextEditingController(),
51+
'hackerrank': TextEditingController(),
52+
'spoj': TextEditingController(),
53+
'leetcode': TextEditingController(),
54+
'old_pass': TextEditingController(),
55+
'new_pass': TextEditingController(),
56+
're_enter': TextEditingController(),
57+
};
58+
59+
void _initializeTextField() {
60+
controllers['name']?.text = _currentUser.fullname;
61+
controllers['username']?.text = _currentUser.username ?? '';
62+
controllers['codechef']?.text = _currentUser.handle?.codechef ?? '';
63+
controllers['codeforces']?.text = _currentUser.handle?.codeforces ?? '';
64+
controllers['hackerrank']?.text = _currentUser.handle?.hackerrank ?? '';
65+
controllers['spoj']?.text = _currentUser.handle?.spoj ?? '';
66+
controllers['leetcode']?.text = _currentUser.handle?.leetcode ?? '';
67+
}
68+
4069
void _onInitialize(Initialize event, Emitter<UpdateProfileState> emit) async {
4170
final _instituteList = await UserRepository.getInstituteList();
4271
if (_instituteList.isNotEmpty) {
4372
institutes = _instituteList;
4473
}
4574

46-
final _controllers = <String, TextEditingController?>{
47-
'codechef': TextEditingController(),
48-
'codeforces': TextEditingController(),
49-
'hackerrank': TextEditingController(),
50-
'spoj': TextEditingController(),
51-
'leetcode': TextEditingController(),
52-
'old_pass': TextEditingController(),
53-
'new_pass': TextEditingController(),
54-
're_enter': TextEditingController(),
55-
};
75+
_initializeTextField();
5676

5777
emit(state.copyWith(
5878
status: const Status(),
5979
user: _currentUser,
60-
controllers: _controllers,
6180
));
6281
}
6382

@@ -75,14 +94,13 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
7594

7695
void _onSwitchView(SwitchView event, Emitter<UpdateProfileState> emit) {
7796
final currentState = state.showChangePasswordView;
78-
final _controllers = state.controllers;
79-
_controllers['old_pass']?.text = '';
80-
_controllers['new_pass']?.text = '';
81-
_controllers['re_enter']?.text = '';
97+
_initializeTextField();
98+
controllers['old_pass']?.text = '';
99+
controllers['new_pass']?.text = '';
100+
controllers['re_enter']?.text = '';
82101
emit(state.copyWith(
83102
showChangePasswordView: !currentState,
84103
activePasswordTextField: -1,
85-
controllers: _controllers,
86104
));
87105
}
88106

@@ -108,8 +126,8 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
108126
UpdatePassword event, Emitter<UpdateProfileState> emit) async {
109127
emit(state.copyWith(isUpdating: true));
110128
try {
111-
await UserRepository.updatePassword(state.controllers['old_pass']!.text,
112-
state.controllers['new_pass']!.text);
129+
await UserRepository.updatePassword(controllers['old_pass']!.text.trim(),
130+
controllers['new_pass']!.text.trim());
113131
add(const SwitchView());
114132
showSnackBar(message: 'Password Changed');
115133
} on Failure catch (err) {
@@ -118,6 +136,108 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
118136
emit(state.copyWith(isUpdating: false));
119137
}
120138

139+
void _onUpdateUserDetails(
140+
UpdateUserDetails event, Emitter<UpdateProfileState> emit) async {
141+
isChanged = false;
142+
emit(state.copyWith(isUpdating: true));
143+
144+
final _errors = <String, dynamic>{};
145+
146+
/// Name Field Validation
147+
isChanged |= controllers['name']!.text.trim() != state.user?.fullname;
148+
if (controllers['name']!.text.isEmpty) {
149+
_errors['name'] = 'Required Field';
150+
}
151+
152+
/// Username Field Validation
153+
if (controllers['username']!.text.isEmpty) {
154+
_errors['username'] = 'Required Field';
155+
} else if (controllers['username']?.text.trim() != _currentUser.username) {
156+
isChanged = true;
157+
final res = await UserRepository.isUsernameAvailable(
158+
controllers['username']!.text.trim());
159+
160+
if (!res) _errors['username'] = 'Already Taken';
161+
}
162+
163+
/// Handles Validation
164+
final platforms = [
165+
'codechef',
166+
'codeforces',
167+
'hackerrank',
168+
'spoj',
169+
'leetcode'
170+
];
171+
for (final platform in platforms) {
172+
final _handle = controllers[platform]!.text.trim();
173+
174+
if (_handle.isEmpty) continue;
175+
176+
if (!compareHandles(platform, _handle)) {
177+
isChanged |= true;
178+
final res = await UserRepository.verifyHandle(platform, _handle);
179+
180+
if (!res) _errors[platform] = 'Invalid Handle';
181+
}
182+
}
183+
184+
/// Returns if errors are not empty
185+
if (_errors.keys.isNotEmpty || !isChanged) {
186+
emit(state.copyWith(isUpdating: false, errors: _errors));
187+
return;
188+
}
189+
190+
final _updatedData = <String, dynamic>{};
191+
_updatedData['fullname'] = controllers['name']?.text.trim();
192+
_updatedData['username'] = controllers['username']?.text.trim();
193+
_updatedData['institute'] = state.user!.institute;
194+
_updatedData['handle.codechef'] = controllers['codechef']?.text.trim();
195+
_updatedData['handle.codeforces'] = controllers['codeforces']?.text.trim();
196+
_updatedData['handle.hackerrank'] = controllers['hackerrank']?.text.trim();
197+
_updatedData['handle.spoj'] = controllers['spoj']?.text.trim();
198+
_updatedData['handle.leetcode'] = controllers['leetcode']?.text.trim();
199+
200+
final statusCode = await UserRepository.updateUserDetails(_updatedData);
201+
202+
if (state.image != null) {
203+
await UserRepository.uploadProfilePicture(state.image!);
204+
}
205+
206+
if (statusCode == 202) {
207+
StorageService.user = await UserRepository.fetchUserDetails();
208+
showSnackBar(message: 'Updated Sucessfully');
209+
Get.back(result: true);
210+
return;
211+
}
212+
213+
emit(state.copyWith(isUpdating: false));
214+
showSnackBar(message: AppStrings.genericError);
215+
}
216+
217+
/// Compares the value from [TextEditingController] and [User]
218+
/// model of that handle. Returns true if they are same
219+
bool compareHandles(String platform, String value) {
220+
switch (platform) {
221+
case 'codechef':
222+
return value == state.user?.handle?.codechef;
223+
224+
case 'codeforces':
225+
return value == state.user?.handle?.codeforces;
226+
227+
case 'hackerrank':
228+
return value == state.user?.handle?.hackerrank;
229+
230+
case 'spoj':
231+
return value == state.user?.handle?.spoj;
232+
233+
case 'leetcode':
234+
return value == state.user?.handle?.leetcode;
235+
236+
default:
237+
return false;
238+
}
239+
}
240+
121241
Future<bool> onWillPop() async {
122242
if (state.showChangePasswordView) {
123243
add(const SwitchView());
@@ -129,4 +249,5 @@ class UpdateProfileBloc extends Bloc<UpdateProfileEvent, UpdateProfileState> {
129249

130250
final User _currentUser = StorageService.user!;
131251
User _updatedUser = StorageService.user!;
252+
bool isChanged = false;
132253
}

lib/presentation/update_profile/bloc/update_profile_event.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,10 @@ class UpdatePassword extends UpdateProfileEvent {
5858
@override
5959
List<Object?> get props => [];
6060
}
61+
62+
class UpdateUserDetails extends UpdateProfileEvent {
63+
const UpdateUserDetails();
64+
65+
@override
66+
List<Object?> get props => [];
67+
}

lib/presentation/update_profile/bloc/update_profile_state.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class UpdateProfileState with _$UpdateProfileState {
99
User? user,
1010
@Default(-1) int activePasswordTextField,
1111
@Default([true, true, true]) List<bool> passwordFieldObscureState,
12-
@Default({}) Map<String, TextEditingController?> controllers,
12+
@Default({}) Map<String, dynamic> errors,
1313
@Default(false) bool isUpdating,
1414
}) = _UpdateProfileState;
1515

lib/presentation/update_profile/update_profile.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ class UpdateProfile extends StatelessWidget {
4141
SizedBox(height: 30.h),
4242
PrimaryButton(
4343
label: 'Save Changes',
44-
onPressed: () {},
44+
onPressed: () {
45+
if (state.isUpdating) return;
46+
context
47+
.read<UpdateProfileBloc>()
48+
.add(const UpdateUserDetails());
49+
},
4550
),
4651
SizedBox(height: 30.h),
4752
],

lib/presentation/update_profile/widgets/change_password.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class ChangePassword extends StatelessWidget {
4141
index: 0,
4242
activeIndex: state.activePasswordTextField,
4343
obscure: state.passwordFieldObscureState[0],
44-
controller: state.controllers['old_pass'],
44+
controller: UpdateProfileBloc.controllers['old_pass'],
4545
),
4646
SizedBox(height: 24.h),
4747
_buildTextField(
@@ -50,7 +50,7 @@ class ChangePassword extends StatelessWidget {
5050
index: 1,
5151
activeIndex: state.activePasswordTextField,
5252
obscure: state.passwordFieldObscureState[1],
53-
controller: state.controllers['new_pass'],
53+
controller: UpdateProfileBloc.controllers['new_pass'],
5454
),
5555
SizedBox(height: 24.h),
5656
_buildTextField(
@@ -59,8 +59,9 @@ class ChangePassword extends StatelessWidget {
5959
index: 2,
6060
activeIndex: state.activePasswordTextField,
6161
obscure: state.passwordFieldObscureState[2],
62-
controller: state.controllers['re_enter'],
63-
compareText: state.controllers['new_pass']?.text,
62+
controller: UpdateProfileBloc.controllers['re_enter'],
63+
compareText:
64+
UpdateProfileBloc.controllers['new_pass']?.text,
6465
),
6566
],
6667
),
@@ -70,6 +71,7 @@ class ChangePassword extends StatelessWidget {
7071
PrimaryButton(
7172
label: 'Update Password',
7273
onPressed: () {
74+
if (state.isUpdating) return;
7375
if (_key.currentState!.validate()) {
7476
context.read<UpdateProfileBloc>().add(const UpdatePassword());
7577
}

0 commit comments

Comments
 (0)