Skip to content

Commit 1fe4625

Browse files
feat: implement feed view
feat: implement feed view
2 parents ac16b6c + dd39f43 commit 1fe4625

25 files changed

Lines changed: 980 additions & 92 deletions

ios/Runner/AppDelegate.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import Flutter
88
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
99
) -> Bool {
1010
GeneratedPluginRegistrant.register(with: self)
11+
12+
if #available(iOS 10.0, *) {
13+
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
14+
}
15+
1116
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1217
}
1318
}

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
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
2+
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
3+
import 'package:timezone/data/latest.dart' as tz;
4+
import 'package:timezone/timezone.dart' as tz;
5+
6+
class NotificationService {
7+
static final _notification = FlutterLocalNotificationsPlugin();
8+
9+
static NotificationDetails _notificationDetails() {
10+
return const NotificationDetails(
11+
android: AndroidNotificationDetails(
12+
'channelId',
13+
'channelName',
14+
importance: Importance.max,
15+
),
16+
iOS: IOSNotificationDetails(),
17+
);
18+
}
19+
20+
static Future init() async {
21+
const settings = InitializationSettings(
22+
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
23+
iOS: IOSInitializationSettings(),
24+
);
25+
26+
await _notification.initialize(
27+
settings,
28+
onSelectNotification: (payload) {},
29+
);
30+
31+
tz.initializeTimeZones();
32+
final locationName = await FlutterNativeTimezone.getLocalTimezone();
33+
tz.setLocalLocation(tz.getLocation(locationName));
34+
}
35+
36+
static Future setNotification({
37+
required DateTime scheduledDate,
38+
int id = 0,
39+
String? title,
40+
String? body,
41+
String? payload,
42+
}) async {
43+
_notification.zonedSchedule(
44+
id,
45+
title,
46+
body,
47+
tz.TZDateTime.from(scheduledDate, tz.local),
48+
_notificationDetails(),
49+
uiLocalNotificationDateInterpretation:
50+
UILocalNotificationDateInterpretation.absoluteTime,
51+
androidAllowWhileIdle: true,
52+
);
53+
}
54+
55+
static Future cancelNotification({String? name}) async {
56+
final pendingNotifications =
57+
await _notification.pendingNotificationRequests();
58+
59+
for (final notification in pendingNotifications) {
60+
if (notification.title == name) {
61+
await _notification.cancel(notification.id);
62+
return;
63+
}
64+
}
65+
}
66+
67+
static Future<List<String?>> getPendingNotification() async {
68+
final pendingNotifications =
69+
await _notification.pendingNotificationRequests();
70+
71+
return pendingNotifications.map((notif) => notif.title).toList();
72+
}
73+
}

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: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:convert';
22

3+
import '../../data/constants/strings.dart';
34
import '../../data/services/remote/api_service.dart';
45
import '../models/activity_details.dart';
56
import '../models/contest.dart';
@@ -38,7 +39,8 @@ class CPRepository {
3839
if (response['status_code'] == 200) {
3940
return Contest.fromJson(response['data']);
4041
}
41-
return null;
42+
43+
throw Exception(AppStrings.genericError);
4244
}
4345

4446
static Future<List<Feed>?> getFeed({DateTime? before}) async {
@@ -53,11 +55,17 @@ class CPRepository {
5355
);
5456

5557
if (response['status_code'] == 200) {
58+
if (response['data'].runtimeType == String &&
59+
response['data'] == 'null') {
60+
return null;
61+
}
62+
5663
return List<Feed>.from(
57-
json.decode(response['data']).map((e) => Feed.fromJson(e)),
64+
response['data'].map((e) => Feed.fromJson(e)),
5865
);
5966
}
60-
return null;
67+
68+
throw Exception(AppStrings.genericError);
6169
}
6270

6371
static Future<List<Submission>?> getSubmissionList(String uid) async {

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/bloc/contests_bloc.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import 'package:freezed_annotation/freezed_annotation.dart';
44

55
import '../../../data/constants/strings.dart';
66
import '../../../data/services/local/storage_service.dart';
7+
import '../../../data/services/remote/notification_service.dart';
78
import '../../../domain/models/contest.dart';
89
import '../../../domain/models/contest_filter.dart';
10+
import '../../../domain/models/status.dart';
911
import '../../../domain/repositories/cp_repository.dart';
12+
import '../../../utils/snackbar.dart';
1013

1114
part 'contests_event.dart';
1215
part 'contests_state.dart';
@@ -21,14 +24,22 @@ class ContestsBloc extends Bloc<ContestsEvent, ContestsState> {
2124
}
2225

2326
void _fetchContests(FetchContests event, Emitter<ContestsState> emit) async {
24-
final contest = await CPRepository.contestList();
27+
Contest? contest;
28+
try {
29+
contest = await CPRepository.contestList();
30+
} on Exception catch (_) {
31+
showSnackBar(message: AppStrings.genericError);
32+
emit(state.copyWith(status: const Status.error(AppStrings.genericError)));
33+
return;
34+
}
35+
2536
_ongoing = [...?contest?.ongoing];
2637
_upcoming = [...?contest?.upcoming];
2738
applyFilter();
2839
final contests = [..._filteredUpcoming, ..._filteredOngoing];
2940
emit(state.copyWith(
3041
contests: contests,
31-
isLoading: false,
42+
status: const Status(),
3243
filter: _filter,
3344
));
3445
}
@@ -92,8 +103,9 @@ class ContestsBloc extends Bloc<ContestsEvent, ContestsState> {
92103
ContestFilter? _filter;
93104
List<Ongoing> _ongoing = [], _filteredOngoing = [];
94105
List<Upcoming> _upcoming = [], _filteredUpcoming = [];
106+
List<String?> pendingNotification = [];
95107

96-
void init() {
108+
void init() async {
97109
final exists = StorageService.exists(AppStrings.filterKey);
98110
if (!exists) {
99111
StorageService.filter = ContestFilter(
@@ -106,9 +118,14 @@ class ContestsBloc extends Bloc<ContestsEvent, ContestsState> {
106118
}
107119

108120
_filter = StorageService.filter;
121+
pendingNotification = await NotificationService.getPendingNotification();
109122
add(const FetchContests());
110123
}
111124

125+
bool reminderSet(String? title) {
126+
return pendingNotification.contains(title);
127+
}
128+
112129
void saveFilter() {
113130
StorageService.filter = _filter;
114131
add(const UpdateContestsList());

0 commit comments

Comments
 (0)