Skip to content

Commit e666d05

Browse files
committed
feat: implement feed view
1 parent ac16b6c commit e666d05

18 files changed

Lines changed: 575 additions & 46 deletions

File tree

lib/data/constants/assets.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class AppAssets {
1919
static const String clock = '$_iconsRoot/clock.svg';
2020
static const String trueRadioButton = '$_iconsRoot/true_radio_button.svg';
2121
static const String falseRadioButton = '$_iconsRoot/false_radio_button.svg';
22+
static const String refresh = '$_iconsRoot/refresh.svg';
23+
static const String userIcon = '$_iconsRoot/default_user_icon.svg';
2224

2325
// Images
2426
static const String circle1 = '$_imagesRoot/circle1.svg';

lib/data/constants/colors.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ class AppColors {
1010
static const Color grey6 = Color(0xFF919191);
1111
static const Color grey7 = Color(0xFFF3F4F7);
1212
static const Color grey8 = Color(0xFFC8C8C8);
13+
static const Color grey9 = Color(0xFFFAFAFA);
14+
static const Color grey10 = Color(0xFFE5E5E5);
15+
static const Color acceptedGreen = Color(0xFF4CAF50);
16+
static const Color errorRed = Color(0xFFEB5757);
1317
static const Color transparent = Colors.transparent;
1418

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

lib/data/constants/styles.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ class AppStyles {
4848
headline5: h5,
4949
headline6: h6,
5050
),
51+
scaffoldBackgroundColor: AppColors.white,
5152
);
5253
}

lib/domain/models/feed.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ part 'feed.g.dart';
88
@freezed
99
class Feed with _$Feed {
1010
factory Feed({
11-
required String userId,
11+
@JsonKey(name: 'user_id') required String userId,
1212
required String username,
1313
required String? fullname,
1414
String? picture,

lib/domain/models/grouped_feed.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ part 'grouped_feed.g.dart';
77
class GroupedFeed with _$GroupedFeed {
88
factory GroupedFeed({
99
required String username,
10-
required String userId,
10+
@JsonKey(name: 'user_id') required String userId,
1111
String? fullname,
1212
String? picture,
1313
String? name,

lib/domain/repositories/cp_repository.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ class CPRepository {
5353
);
5454

5555
if (response['status_code'] == 200) {
56+
if (response['data'].runtimeType == String &&
57+
response['data'] == 'null') {
58+
return null;
59+
}
60+
5661
return List<Feed>.from(
57-
json.decode(response['data']).map((e) => Feed.fromJson(e)),
62+
response['data'].map((e) => Feed.fromJson(e)),
5863
);
5964
}
6065
return null;

lib/presentation/contests/widgets/empty_state.dart renamed to lib/presentation/components/widgets/empty_state.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
33
import 'package:flutter_svg/flutter_svg.dart';
44

55
import '../../../data/constants/assets.dart';
6-
import '../../../data/constants/colors.dart';
76

87
class EmptyState extends StatelessWidget {
9-
const EmptyState({Key? key}) : super(key: key);
8+
const EmptyState({required this.description, Key? key}) : super(key: key);
9+
final String description;
1010

1111
@override
1212
Widget build(BuildContext context) {
@@ -17,12 +17,10 @@ class EmptyState extends StatelessWidget {
1717
Container(
1818
width: double.infinity,
1919
padding: EdgeInsets.all(25.r),
20-
child: const Text(
21-
'No contests found, please adjust your filters!',
20+
child: Text(
21+
description,
2222
textAlign: TextAlign.center,
23-
style: TextStyle(
24-
color: AppColors.grey1,
25-
),
23+
style: Theme.of(context).textTheme.headline6,
2624
),
2725
),
2826
],

lib/presentation/contests/contests_screen.dart

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33

4-
import '../../data/constants/colors.dart';
54
import '../../domain/models/contest.dart';
5+
import '../components/widgets/empty_state.dart';
66
import 'bloc/contests_bloc.dart';
77
import 'widgets/contest_card.dart';
88
import 'widgets/contest_header.dart';
9-
import 'widgets/empty_state.dart';
109
import 'widgets/loading_state.dart';
1110

1211
class ContestsScreen extends StatelessWidget {
@@ -16,40 +15,37 @@ class ContestsScreen extends StatelessWidget {
1615
Widget build(BuildContext context) {
1716
return BlocProvider<ContestsBloc>(
1817
create: (_) => ContestsBloc()..init(),
19-
child: BlocBuilder<ContestsBloc, ContestsState>(
20-
builder: (context, state) {
21-
return Scaffold(
22-
backgroundColor: AppColors.white,
23-
appBar: const PreferredSize(
24-
preferredSize: Size.fromHeight(kToolbarHeight),
25-
child: ContestHeader(),
26-
),
27-
body: Builder(
28-
builder: (_) {
29-
if (state.isLoading) {
30-
return const LoadingState();
18+
child: Scaffold(
19+
appBar: const PreferredSize(
20+
preferredSize: Size.fromHeight(kToolbarHeight),
21+
child: ContestHeader(),
22+
),
23+
body: BlocBuilder<ContestsBloc, ContestsState>(
24+
builder: (context, state) {
25+
if (state.isLoading) {
26+
return const LoadingState();
27+
}
28+
if (state.contests.isEmpty) {
29+
return const EmptyState(
30+
description: 'No contests found, please adjust your filters!',
31+
);
32+
}
33+
return ListView.builder(
34+
itemCount: state.contests.length,
35+
itemBuilder: (context, index) {
36+
if (state.contests[index] is Ongoing) {
37+
return ContestCard(
38+
ongoing: state.contests[index],
39+
);
3140
}
32-
if (state.contests.isEmpty) {
33-
return const EmptyState();
34-
}
35-
return ListView.builder(
36-
itemCount: state.contests.length,
37-
itemBuilder: (context, index) {
38-
if (state.contests[index] is Ongoing) {
39-
return ContestCard(
40-
ongoing: state.contests[index],
41-
);
42-
}
4341

44-
return ContestCard(
45-
upcoming: state.contests[index],
46-
);
47-
},
42+
return ContestCard(
43+
upcoming: state.contests[index],
4844
);
4945
},
50-
),
51-
);
52-
},
46+
);
47+
},
48+
),
5349
),
5450
);
5551
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:flutter/widgets.dart';
3+
import 'package:flutter_bloc/flutter_bloc.dart';
4+
import 'package:freezed_annotation/freezed_annotation.dart';
5+
import 'package:get/get.dart';
6+
7+
import '../../../domain/models/feed.dart';
8+
import '../../../domain/models/grouped_feed.dart';
9+
import '../../../domain/repositories/cp_repository.dart';
10+
11+
part 'feed_bloc.freezed.dart';
12+
part 'feed_state.dart';
13+
part 'feed_event.dart';
14+
15+
class FeedBloc extends Bloc<FeedEvent, FeedState> {
16+
FeedBloc() : super(const FeedState()) {
17+
on<FeedFetch>(_onFeedFetch);
18+
}
19+
20+
void _onFeedFetch(FeedFetch event, Emitter<FeedState> emit) async {
21+
if (state.isFetchingNext) return;
22+
23+
if (event.fetchNext) {
24+
emit(state.copyWith(isFetchingNext: true));
25+
} else {
26+
emit(state.copyWith(isLoading: true));
27+
}
28+
29+
feeds = await CPRepository.getFeed(
30+
before: event.fetchNext
31+
? groupedFeeds.last.submissions?.last.createdAt
32+
: null,
33+
) ??
34+
<Feed>[];
35+
36+
if (feeds.isEmpty) _allLoaded = true;
37+
38+
for (final feed in feeds) {
39+
final groupFeed = groupedFeeds.firstWhere(
40+
(element) => element.name == feed.submission?.name,
41+
orElse: () {
42+
groupedFeeds.add(feed.toGroupedFeed());
43+
return groupedFeeds.last;
44+
},
45+
);
46+
groupFeed.submissions!.add(
47+
Submissions(
48+
createdAt: feed.submission?.createdAt,
49+
status: feed.submission?.status,
50+
points: feed.submission?.points,
51+
tags: feed.submission?.tags,
52+
rating: feed.submission?.rating,
53+
),
54+
);
55+
}
56+
57+
emit(
58+
state.copyWith(
59+
isLoading: false,
60+
feeds: [...groupedFeeds],
61+
isFetchingNext: false,
62+
allLoaded: _allLoaded,
63+
),
64+
);
65+
}
66+
67+
void _onScrolled() {
68+
final hasReachedEnd = scrollController.position.maxScrollExtent -
69+
scrollController.position.pixels <=
70+
MediaQuery.of(Get.context!).size.width / 2;
71+
72+
if (hasReachedEnd && !_allLoaded) {
73+
add(const FeedFetch(fetchNext: true));
74+
}
75+
}
76+
77+
void init() {
78+
add(const FeedFetch());
79+
scrollController.addListener(_onScrolled);
80+
}
81+
82+
List<Feed> feeds = <Feed>[];
83+
List<GroupedFeed> groupedFeeds = <GroupedFeed>[];
84+
bool _allLoaded = false;
85+
final ScrollController scrollController = ScrollController();
86+
}
87+
88+
extension on Feed {
89+
GroupedFeed toGroupedFeed() {
90+
return GroupedFeed(
91+
username: username,
92+
userId: userId,
93+
fullname: fullname,
94+
picture: picture,
95+
name: submission?.name,
96+
url: submission?.url,
97+
language: submission?.language,
98+
submissions: [],
99+
);
100+
}
101+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
part of 'feed_bloc.dart';
2+
3+
abstract class FeedEvent extends Equatable {
4+
const FeedEvent();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
class FeedFetch extends FeedEvent {
11+
const FeedFetch({this.fetchNext = false});
12+
13+
final bool fetchNext;
14+
15+
@override
16+
List<Object?> get props => [fetchNext];
17+
}

0 commit comments

Comments
 (0)