Skip to content

Commit 4aecd03

Browse files
committed
feat: implement profile view
1 parent 1fe4625 commit 4aecd03

18 files changed

Lines changed: 1230 additions & 21 deletions

lib/data/constants/colors.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22

33
class AppColors {
44
static const Color white = Colors.white;
5+
static const Color white1 = Color(0xFFE5E5E5);
56
static const Color grey1 = Color(0xFF979797);
67
static const Color grey2 = Color(0xFFC5CEE0);
78
static const Color grey3 = Color(0xFF242424);
@@ -14,6 +15,7 @@ class AppColors {
1415
static const Color grey10 = Color(0xFFE5E5E5);
1516
static const Color acceptedGreen = Color(0xFF4CAF50);
1617
static const Color errorRed = Color(0xFFEB5757);
18+
static const Color lightBlue = Color(0xFFEFF2FC);
1719
static const Color transparent = Colors.transparent;
1820

1921
static const Color primary = Color(0xFF3366FF);

lib/domain/models/following.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ part 'following.g.dart';
66
@freezed
77
class Following with _$Following {
88
factory Following({
9-
required String id,
9+
@JsonKey(name: '_id') required String id,
1010
required String username,
1111
String? fullname,
1212
String? picture,

lib/domain/models/user.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ class User with _$User {
1515
Handle? handle,
1616
String? id,
1717
String? institute,
18-
int? noOfFollowing,
18+
@JsonKey(name: 'no_of_following') int? noOfFollowing,
1919
String? picture,
2020
UserProfile? profiles,
21-
List<Submission>? recentSubmissions,
21+
@JsonKey(name: 'recent_submissions') List<Submission>? recentSubmissions,
2222
String? username,
23-
SolvedProblemsCount? solvedProblemsCount,
23+
@JsonKey(name: 'solved_problems_count')
24+
SolvedProblemsCount? solvedProblemsCount,
2425
}) = _User;
2526

2627
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

lib/domain/repositories/user_repository.dart

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../../data/services/local/storage_service.dart';
66
import '../../data/services/remote/api_service.dart';
77
import '../../utils/auth_token.dart' as auth_token_utils;
88
import '../../utils/failures.dart';
9+
import '../models/activity_details.dart';
910
import '../models/following.dart';
1011
import '../models/sign_up.dart';
1112
import '../models/submission_status.dart';
@@ -192,7 +193,7 @@ class UserRepository {
192193

193194
if (response['status_code'] == 200) {
194195
return List<Following>.from(
195-
json.decode(response['data']).map((e) => Following.fromJson(e)),
196+
response['data'].map((e) => Following.fromJson(e)),
196197
);
197198
}
198199
return null;
@@ -245,7 +246,7 @@ class UserRepository {
245246
return [];
246247
}
247248

248-
static Future<List<SubmissionStatus>?> getSubmissionStatusData(
249+
static Future<SubmissionStatus?> getSubmissionStatusData(
249250
String id,
250251
) async {
251252
final endpoint = 'graph/status/$id';
@@ -258,8 +259,24 @@ class UserRepository {
258259
);
259260

260261
if (response['status_code'] == 200) {
261-
return List<SubmissionStatus>.from(
262-
json.decode(response['data'])?.map((e) => SubmissionStatus.fromJson(e)),
262+
return SubmissionStatus.fromJson(response['data']);
263+
}
264+
return null;
265+
}
266+
267+
static Future<List<ActivityDetails>?> getActivityDetails(String id) async {
268+
final endpoint = 'graph/activity/$id';
269+
final headers = <String, dynamic>{};
270+
271+
ApiService.addTokenToHeaders(headers);
272+
final response = await ApiService.get(
273+
endpoint,
274+
headers: headers,
275+
);
276+
277+
if (response['status_code'] == 200) {
278+
return List<ActivityDetails>.from(
279+
response['data'].map((e) => ActivityDetails.fromJson(e)),
263280
);
264281
}
265282
return null;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'dart:math' as math;
2+
import 'dart:ui';
3+
4+
import 'package:equatable/equatable.dart';
5+
import 'package:flutter_bloc/flutter_bloc.dart';
6+
import 'package:freezed_annotation/freezed_annotation.dart';
7+
8+
import '../../../data/services/local/storage_service.dart';
9+
import '../../../domain/models/activity_details.dart';
10+
import '../../../domain/models/following.dart';
11+
import '../../../domain/models/submission.dart';
12+
import '../../../domain/models/submission_status.dart';
13+
import '../../../domain/models/user.dart';
14+
import '../../../domain/repositories/user_repository.dart';
15+
16+
part 'profile_event.dart';
17+
part 'profile_state.dart';
18+
part 'profile_bloc.freezed.dart';
19+
20+
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
21+
ProfileBloc() : super(const ProfileState()) {
22+
on<FetchDetails>(_onFetchDetails);
23+
on<UpdateYear>(_onUpdateYear);
24+
on<UpdateMonth>(_onUpdateMonth);
25+
}
26+
27+
void _onFetchDetails(FetchDetails event, Emitter<ProfileState> emit) async {
28+
if (!state.isLoading) emit(state.copyWith(isLoading: true));
29+
30+
// Fetch user details
31+
final User? _user;
32+
if (event.userId.isEmpty) {
33+
_user = StorageService.user;
34+
} else {
35+
_user = await UserRepository.fetchUserDetails(uid: event.userId);
36+
}
37+
38+
final _followingList = await UserRepository.getFollowingList();
39+
40+
final _subStats = await UserRepository.getSubmissionStatusData(_user!.id!);
41+
42+
final _submission = _user.recentSubmissions;
43+
44+
final _activityDetails = await UserRepository.getActivityDetails(_user.id!);
45+
46+
final _activity = <DateTime, dynamic>{};
47+
for (final activity in _activityDetails ?? <ActivityDetails>[]) {
48+
if (activity.createdAt == null) continue;
49+
_activity[activity.createdAt!] = activity.correct;
50+
}
51+
52+
final _mostRecentSubmission = <Submission>[];
53+
for (var index = 0;
54+
index < (_submission?.length ?? 0) && index < 2;
55+
index++) {
56+
_mostRecentSubmission.add(_submission![index]);
57+
}
58+
59+
bool? _isFollowing;
60+
for (final follower in _followingList ?? <Following>[]) {
61+
if (follower.id == event.userId) {
62+
_isFollowing = true;
63+
break;
64+
}
65+
}
66+
67+
_isFollowing ??= false;
68+
69+
emit(state.copyWith(
70+
isLoading: false,
71+
user: _user,
72+
following: _followingList,
73+
submissionStatus: _subStats,
74+
submission: _submission,
75+
activity: _activity,
76+
personalProfile: event.userId.isEmpty,
77+
isFollowing: _isFollowing,
78+
currentYear: _currentYear,
79+
currentTriplet: _currentTriplet,
80+
));
81+
}
82+
83+
void _onUpdateYear(UpdateYear event, Emitter<ProfileState> emit) {
84+
if (event.increment) {
85+
_currentYear++;
86+
} else {
87+
_currentYear--;
88+
}
89+
90+
emit(state.copyWith(currentYear: _currentYear));
91+
}
92+
93+
void _onUpdateMonth(UpdateMonth event, Emitter<ProfileState> emit) {
94+
if (event.increment) {
95+
if (_currentTriplet == 3) _currentYear++;
96+
_currentTriplet++;
97+
} else {
98+
if (_currentTriplet == 0) _currentYear--;
99+
_currentTriplet--;
100+
}
101+
102+
_currentTriplet %= 4;
103+
104+
emit(state.copyWith(
105+
currentTriplet: _currentTriplet,
106+
currentYear: _currentYear,
107+
));
108+
}
109+
110+
// Index -> Index%4
111+
static List<String> monthTriplet(int index) {
112+
switch (index) {
113+
case 0:
114+
return ['Jan', 'Feb', 'Mar'];
115+
116+
case 1:
117+
return ['Apr', 'May', 'Jun'];
118+
119+
case 2:
120+
return ['Jul', 'Aug', 'Sep'];
121+
122+
default:
123+
return ['Oct', 'Nov', 'Dec'];
124+
}
125+
}
126+
127+
// Cell Color for Acceptance Graph
128+
static Color getCellColor(int index) {
129+
if (index < 0) {
130+
return Color.fromRGBO(255, 0, 0, math.min(-0.2 * index, 1));
131+
} else if (index > 0) {
132+
return Color.fromRGBO(0, 255, 0, math.min(0.2 * index, 1));
133+
}
134+
135+
// Default cell color
136+
return const Color(0xFFEEEEEE);
137+
}
138+
139+
int _currentYear = DateTime.now().year;
140+
int _currentTriplet = DateTime.now().month ~/ 3;
141+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
part of 'profile_bloc.dart';
2+
3+
abstract class ProfileEvent extends Equatable {
4+
const ProfileEvent();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
class FetchDetails extends ProfileEvent {
11+
const FetchDetails({this.userId = ''});
12+
13+
final String userId;
14+
15+
@override
16+
List<Object?> get props => [userId];
17+
}
18+
19+
class UpdateYear extends ProfileEvent {
20+
const UpdateYear({required this.increment});
21+
22+
final bool increment;
23+
24+
@override
25+
List<Object?> get props => [increment];
26+
}
27+
28+
class UpdateMonth extends ProfileEvent {
29+
const UpdateMonth({required this.increment});
30+
31+
final bool increment;
32+
33+
@override
34+
List<Object?> get props => [increment];
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
part of 'profile_bloc.dart';
2+
3+
@freezed
4+
class ProfileState with _$ProfileState {
5+
const factory ProfileState({
6+
@Default(true) bool isLoading,
7+
@Default(true) bool personalProfile,
8+
@Default(false) bool isFollowing,
9+
User? user,
10+
List<Following>? following,
11+
SubmissionStatus? submissionStatus,
12+
List<Submission>? submission,
13+
Map<DateTime, dynamic>? activity,
14+
int? currentYear,
15+
int? currentTriplet,
16+
}) = _ProfileState;
17+
18+
const ProfileState._();
19+
}
Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,63 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:flutter_screenutil/flutter_screenutil.dart';
4+
5+
import '../../data/constants/styles.dart';
6+
import 'bloc/profile_bloc.dart';
7+
import 'widgets/acceptance_graph.dart';
8+
import 'widgets/accuracy_display.dart';
9+
import 'widgets/loading_state.dart';
10+
import 'widgets/profile_header.dart';
11+
import 'widgets/question_solved.dart';
12+
import 'widgets/submission_display.dart';
213

314
class ProfileScreen extends StatelessWidget {
415
const ProfileScreen({Key? key}) : super(key: key);
516

617
@override
718
Widget build(BuildContext context) {
8-
return const Center(
9-
child: Text('Profile Screen'),
19+
return BlocProvider(
20+
create: (context) => ProfileBloc()..add(const FetchDetails()),
21+
child: BlocBuilder<ProfileBloc, ProfileState>(
22+
// When Loading state changes
23+
buildWhen: (previous, current) =>
24+
previous.isLoading ^ current.isLoading,
25+
builder: (context, state) {
26+
if (state.isLoading) return const ProfileLoadingState();
27+
28+
return ListView(
29+
children: [
30+
const ProfileHeader(),
31+
Padding(
32+
padding: EdgeInsets.fromLTRB(16.w, 24.h, 0, 8.h),
33+
child: Text(
34+
'Accuracy',
35+
style: AppStyles.h1.copyWith(fontSize: 15.sp),
36+
),
37+
),
38+
const AccuracyDisplay(),
39+
SizedBox(height: 30.h),
40+
Padding(
41+
padding: EdgeInsets.only(left: 16.w, top: 24.h),
42+
child: Text(
43+
'Number of question solved',
44+
style: AppStyles.h1.copyWith(fontSize: 15.sp),
45+
),
46+
),
47+
const QuestionSolved(),
48+
Padding(
49+
padding: EdgeInsets.only(left: 16.w, top: 24.h),
50+
child: Text(
51+
'Status of total Submissions',
52+
style: AppStyles.h1.copyWith(fontSize: 15.sp),
53+
),
54+
),
55+
const SubmissionDisplay(),
56+
const AcceptanceGraph(),
57+
],
58+
);
59+
},
60+
),
1061
);
1162
}
1263
}

0 commit comments

Comments
 (0)