Skip to content

Commit 8f9f611

Browse files
committed
feat: implement headline filter bottom sheet
- Added HeadlineFilter model - Created filter bottom sheet UI - Implemented filter apply logic
1 parent 36b82e2 commit 8f9f611

File tree

8 files changed

+327
-32
lines changed

8 files changed

+327
-32
lines changed

lib/app/view/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class _AppView extends StatelessWidget {
3939
return BlocBuilder<AppBloc, AppState>(
4040
builder: (context, state) {
4141
return MaterialApp.router(
42+
debugShowCheckedModeBanner: false,
4243
theme:
4344
state.themeMode == ThemeMode.light ? lightTheme() : darkTheme(),
4445
routerConfig: appRouter,

lib/headlines-feed/bloc/headlines_feed_bloc.dart

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:bloc/bloc.dart';
22
import 'package:bloc_concurrency/bloc_concurrency.dart';
33
import 'package:equatable/equatable.dart';
44
import 'package:ht_headlines_repository/ht_headlines_repository.dart';
5+
import 'package:ht_main/headlines-feed/models/headline_filter.dart';
56

67
part 'headlines_feed_event.dart';
78
part 'headlines_feed_state.dart';
@@ -16,7 +17,7 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
1617
/// {@macro headlines_feed_bloc}
1718
HeadlinesFeedBloc({required HtHeadlinesRepository headlinesRepository})
1819
: _headlinesRepository = headlinesRepository,
19-
super(HeadlinesFeedInitial()) {
20+
super(HeadlinesFeedLoading()) {
2021
on<HeadlinesFeedFetchRequested>(
2122
_onHeadlinesFeedFetchRequested,
2223
transformer: sequential(),
@@ -25,10 +26,51 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
2526
_onHeadlinesFeedRefreshRequested,
2627
transformer: restartable(),
2728
);
29+
on<HeadlinesFeedFilterChanged>(
30+
_onHeadlinesFeedFilterChanged,
31+
);
2832
}
2933

3034
final HtHeadlinesRepository _headlinesRepository;
3135

36+
Future<void> _onHeadlinesFeedFilterChanged(
37+
HeadlinesFeedFilterChanged event,
38+
Emitter<HeadlinesFeedState> emit,
39+
) async {
40+
emit(HeadlinesFeedLoading());
41+
try {
42+
final response = await _headlinesRepository.getHeadlines(
43+
limit: 20,
44+
category: event.category, // Pass category directly
45+
source: event.source, // Pass source directly
46+
eventCountry: event.eventCountry, // Pass eventCountry directly
47+
);
48+
final newFilter = (state is HeadlinesFeedLoaded)
49+
? (state as HeadlinesFeedLoaded).filter.copyWith(
50+
category: event.category,
51+
source: event.source,
52+
eventCountry: event.eventCountry,
53+
)
54+
: HeadlineFilter(
55+
category: event.category,
56+
source: event.source,
57+
eventCountry: event.eventCountry,
58+
);
59+
emit(
60+
HeadlinesFeedLoaded(
61+
headlines: response.items,
62+
hasMore: response.hasMore,
63+
cursor: response.cursor,
64+
filter: newFilter,
65+
),
66+
);
67+
} on HeadlinesFetchException catch (e) {
68+
emit(HeadlinesFeedError(message: e.message));
69+
} catch (_) {
70+
emit(const HeadlinesFeedError(message: 'An unexpected error occurred'));
71+
}
72+
}
73+
3274
/// Handles [HeadlinesFeedFetchRequested] events.
3375
///
3476
/// Fetches headlines from the repository and emits
@@ -46,12 +88,16 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
4688
final response = await _headlinesRepository.getHeadlines(
4789
limit: 20,
4890
startAfterId: currentState.cursor,
91+
category: currentState.filter.category, // Use existing filter
92+
source: currentState.filter.source, // Use existing filter
93+
eventCountry: currentState.filter.eventCountry, // Use existing filter
4994
);
5095
emit(
5196
HeadlinesFeedLoaded(
5297
headlines: currentState.headlines + response.items,
5398
hasMore: response.hasMore,
5499
cursor: response.cursor,
100+
filter: currentState.filter,
55101
),
56102
);
57103
} on HeadlinesFetchException catch (e) {
@@ -62,12 +108,26 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
62108
} else {
63109
emit(HeadlinesFeedLoading());
64110
try {
65-
final response = await _headlinesRepository.getHeadlines(limit: 20);
111+
final response = await _headlinesRepository.getHeadlines(
112+
limit: 20,
113+
category: state is HeadlinesFeedLoaded
114+
? (state as HeadlinesFeedLoaded).filter.category
115+
: null,
116+
source: state is HeadlinesFeedLoaded
117+
? (state as HeadlinesFeedLoaded).filter.source
118+
: null,
119+
eventCountry: state is HeadlinesFeedLoaded
120+
? (state as HeadlinesFeedLoaded).filter.eventCountry
121+
: null,
122+
);
66123
emit(
67124
HeadlinesFeedLoaded(
68125
headlines: response.items,
69126
hasMore: response.hasMore,
70127
cursor: response.cursor,
128+
filter: state is HeadlinesFeedLoaded
129+
? (state as HeadlinesFeedLoaded).filter
130+
: const HeadlineFilter(),
71131
),
72132
);
73133
} on HeadlinesFetchException catch (e) {
@@ -92,12 +152,26 @@ class HeadlinesFeedBloc extends Bloc<HeadlinesFeedEvent, HeadlinesFeedState> {
92152
) async {
93153
emit(HeadlinesFeedLoading());
94154
try {
95-
final response = await _headlinesRepository.getHeadlines(limit: 20);
155+
final response = await _headlinesRepository.getHeadlines(
156+
limit: 20,
157+
category: state is HeadlinesFeedLoaded
158+
? (state as HeadlinesFeedLoaded).filter.category
159+
: null,
160+
source: state is HeadlinesFeedLoaded
161+
? (state as HeadlinesFeedLoaded).filter.source
162+
: null,
163+
eventCountry: state is HeadlinesFeedLoaded
164+
? (state as HeadlinesFeedLoaded).filter.eventCountry
165+
: null,
166+
);
96167
emit(
97168
HeadlinesFeedLoaded(
98169
headlines: response.items,
99170
hasMore: response.hasMore,
100171
cursor: response.cursor,
172+
filter: state is HeadlinesFeedLoaded
173+
? (state as HeadlinesFeedLoaded).filter
174+
: const HeadlineFilter(),
101175
),
102176
);
103177
} on HeadlinesFetchException catch (e) {

lib/headlines-feed/bloc/headlines_feed_event.dart

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sealed class HeadlinesFeedEvent extends Equatable {
88
const HeadlinesFeedEvent();
99

1010
@override
11-
List<Object> get props => [];
11+
List<Object?> get props => [];
1212
}
1313

1414
/// {@template headlines_feed_fetch_requested}
@@ -22,10 +22,34 @@ final class HeadlinesFeedFetchRequested extends HeadlinesFeedEvent {
2222
final String? cursor;
2323

2424
@override
25-
List<Object> get props => [cursor ?? ''];
25+
List<Object?> get props => [cursor];
2626
}
2727

2828
/// {@template headlines_feed_refresh_requested}
2929
/// Event triggered when the headlines feed needs to be refreshed.
3030
/// {@endtemplate}
3131
final class HeadlinesFeedRefreshRequested extends HeadlinesFeedEvent {}
32+
33+
/// {@template headlines_feed_filter_changed}
34+
/// Event triggered when the filter parameters for the headlines feed change.
35+
/// {@endtemplate}
36+
final class HeadlinesFeedFilterChanged extends HeadlinesFeedEvent {
37+
/// {@macro headlines_feed_filter_changed}
38+
const HeadlinesFeedFilterChanged({
39+
this.category,
40+
this.source,
41+
this.eventCountry,
42+
});
43+
44+
/// The selected category filter.
45+
final String? category;
46+
47+
/// The selected source filter.
48+
final String? source;
49+
50+
/// The selected event country filter.
51+
final String? eventCountry;
52+
53+
@override
54+
List<Object?> get props => [category, source, eventCountry];
55+
}

lib/headlines-feed/bloc/headlines_feed_state.dart

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,25 @@ sealed class HeadlinesFeedState extends Equatable {
88
const HeadlinesFeedState();
99

1010
@override
11-
List<Object> get props => [];
11+
List<Object?> get props => [];
1212
}
1313

14-
/// {@template headlines_feed_initial}
15-
/// The initial state of the headlines feed.
16-
/// {@endtemplate}
17-
final class HeadlinesFeedInitial extends HeadlinesFeedState {}
18-
1914
/// {@template headlines_feed_loading}
2015
/// State indicating that the headlines feed is being loaded.
2116
/// {@endtemplate}
2217
final class HeadlinesFeedLoading extends HeadlinesFeedState {}
2318

2419
/// {@template headlines_feed_loaded}
25-
/// State indicating that the headlines feed has been loaded successfully.
20+
/// State indicating that the headlines feed has been loaded successfully,
21+
/// potentially with applied filters.
2622
/// {@endtemplate}
2723
final class HeadlinesFeedLoaded extends HeadlinesFeedState {
2824
/// {@macro headlines_feed_loaded}
2925
const HeadlinesFeedLoaded({
30-
required this.headlines,
31-
required this.hasMore,
26+
this.headlines = const [],
27+
this.hasMore = true,
3228
this.cursor,
29+
this.filter = const HeadlineFilter(),
3330
});
3431

3532
/// The headlines data.
@@ -41,8 +38,27 @@ final class HeadlinesFeedLoaded extends HeadlinesFeedState {
4138
/// The cursor for the next page.
4239
final String? cursor;
4340

41+
/// The filter applied to the headlines.
42+
final HeadlineFilter filter;
43+
44+
/// Creates a copy of this [HeadlinesFeedLoaded] with the given fields
45+
/// replaced with the new values.
46+
HeadlinesFeedLoaded copyWith({
47+
List<Headline>? headlines,
48+
bool? hasMore,
49+
String? cursor,
50+
HeadlineFilter? filter,
51+
}) {
52+
return HeadlinesFeedLoaded(
53+
headlines: headlines ?? this.headlines,
54+
hasMore: hasMore ?? this.hasMore,
55+
cursor: cursor ?? this.cursor,
56+
filter: filter ?? this.filter,
57+
);
58+
}
59+
4460
@override
45-
List<Object> get props => [headlines, hasMore, cursor ?? ''];
61+
List<Object?> get props => [headlines, hasMore, cursor, filter];
4662
}
4763

4864
/// {@template headlines_feed_error}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:equatable/equatable.dart';
2+
3+
/// {@template headline_filter}
4+
/// A model representing the filter parameters for headlines.
5+
/// {@endtemplate}
6+
class HeadlineFilter extends Equatable {
7+
/// {@macro headline_filter}
8+
const HeadlineFilter({
9+
this.category,
10+
this.source,
11+
this.eventCountry,
12+
});
13+
14+
/// The selected category filter.
15+
final String? category;
16+
17+
/// The selected source filter.
18+
final String? source;
19+
20+
/// The selected event country filter.
21+
final String? eventCountry;
22+
23+
@override
24+
List<Object?> get props => [category, source, eventCountry];
25+
26+
/// Creates a copy of this [HeadlineFilter] with the given fields
27+
/// replaced with the new values.
28+
HeadlineFilter copyWith({
29+
String? category,
30+
String? source,
31+
String? eventCountry,
32+
}) {
33+
return HeadlineFilter(
34+
category: category ?? this.category,
35+
source: source ?? this.source,
36+
eventCountry: eventCountry ?? this.eventCountry,
37+
);
38+
}
39+
}

0 commit comments

Comments
 (0)