Skip to content

Commit b5e9493

Browse files
committed
feat(feed): multi-select filters for headlines
- Implemented multi-select categories/sources - Added multi-select countries filters - Updated filter logic to handle lists
1 parent 86ca36a commit b5e9493

File tree

1 file changed

+100
-98
lines changed

1 file changed

+100
-98
lines changed

lib/headlines-feed/view/headlines_feed_page.dart

Lines changed: 100 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart';
77
import 'package:ht_main/l10n/l10n.dart';
88
import 'package:ht_main/router/routes.dart';
99
import 'package:ht_main/shared/constants/constants.dart';
10+
import 'package:ht_categories_client/ht_categories_client.dart'; // Import Category
11+
import 'package:ht_countries_client/ht_countries_client.dart'; // Import Country
1012
import 'package:ht_main/shared/widgets/failure_state_widget.dart';
1113
import 'package:ht_main/shared/widgets/loading_state_widget.dart';
14+
import 'package:ht_sources_client/ht_sources_client.dart'; // Import Source
1215

1316
class HeadlinesFeedPage extends StatelessWidget {
1417
const HeadlinesFeedPage({super.key});
@@ -93,10 +96,11 @@ class _HeadlinesFeedViewState extends State<_HeadlinesFeedView> {
9396
builder: (context, state) {
9497
var isFilterApplied = false;
9598
if (state is HeadlinesFeedLoaded) {
99+
// Check if any filter list is non-null and not empty
96100
isFilterApplied =
97-
state.filter.category != null ||
98-
state.filter.source != null ||
99-
state.filter.eventCountry != null;
101+
(state.filter.categories?.isNotEmpty ?? false) ||
102+
(state.filter.sources?.isNotEmpty ?? false) ||
103+
(state.filter.eventCountries?.isNotEmpty ?? false);
100104
}
101105
return Stack(
102106
children: [
@@ -210,18 +214,20 @@ class _HeadlinesFilterBottomSheet extends StatefulWidget {
210214

211215
class _HeadlinesFilterBottomSheetState
212216
extends State<_HeadlinesFilterBottomSheet> {
213-
String? selectedCategory;
214-
String? selectedSource;
215-
String? selectedEventCountry;
217+
// Use lists to store selected filters
218+
List<Category> selectedCategories = [];
219+
List<Source> selectedSources = [];
220+
List<Country> selectedEventCountries = [];
216221

217222
@override
218223
void initState() {
219224
super.initState();
220225
final state = widget.bloc.state;
221226
if (state is HeadlinesFeedLoaded) {
222-
selectedCategory = state.filter.category;
223-
selectedSource = state.filter.source;
224-
selectedEventCountry = state.filter.eventCountry;
227+
// Initialize lists from the current filter state, handle nulls
228+
selectedCategories = List.from(state.filter.categories ?? []);
229+
selectedSources = List.from(state.filter.sources ?? []);
230+
selectedEventCountries = List.from(state.filter.eventCountries ?? []);
225231
}
226232
}
227233

@@ -247,104 +253,92 @@ class _HeadlinesFilterBottomSheetState
247253
textAlign: TextAlign.center,
248254
),
249255
const SizedBox(height: AppSpacing.xl),
250-
// Category Dropdown
251-
DropdownButtonFormField<String>(
252-
decoration: InputDecoration(
253-
labelText: l10n.headlinesFeedFilterCategoryLabel,
254-
),
255-
value: selectedCategory,
256-
// TODO(fulleni): Populate items dynamically from repository/config
257-
items: [
258-
DropdownMenuItem<String>(
259-
child: Text(l10n.headlinesFeedFilterAllOption),
260-
),
261-
DropdownMenuItem(
262-
value: 'technology',
263-
child: Text(l10n.headlinesFeedFilterCategoryTechnology),
264-
),
265-
DropdownMenuItem(
266-
value: 'business',
267-
child: Text(l10n.headlinesFeedFilterCategoryBusiness),
268-
),
269-
DropdownMenuItem(
270-
value: 'sports',
271-
child: Text(l10n.headlinesFeedFilterCategorySports),
256+
257+
// --- Category Filters ---
258+
Text(
259+
l10n.headlinesFeedFilterCategoryLabel,
260+
style: Theme.of(context).textTheme.titleMedium,
261+
),
262+
const SizedBox(height: AppSpacing.md),
263+
// TODO(cline): Implement multi-select UI for categories
264+
// Fetch available categories from a repository
265+
// Use Wrap + FilterChip to display options
266+
// Update selectedCategories list in setState when chips are toggled
267+
// Example placeholder:
268+
Container(
269+
padding: const EdgeInsets.all(AppSpacing.md),
270+
decoration: BoxDecoration(
271+
border: Border.all(
272+
color: Theme.of(context).colorScheme.outline,
272273
),
273-
],
274-
onChanged: (value) {
275-
setState(() {
276-
selectedCategory = value;
277-
});
278-
},
274+
borderRadius: BorderRadius.circular(AppSpacing.sm),
275+
),
276+
child: const Text('Category FilterChip UI Placeholder'),
279277
),
280278
const SizedBox(height: AppSpacing.lg),
281-
// Source Dropdown
282-
DropdownButtonFormField<String>(
283-
decoration: InputDecoration(
284-
labelText: l10n.headlinesFeedFilterSourceLabel,
285-
),
286-
value: selectedSource,
287-
// TODO(fulleni): Populate items dynamically
288-
items: [
289-
DropdownMenuItem<String>(
290-
child: Text(l10n.headlinesFeedFilterAllOption),
291-
),
292-
DropdownMenuItem(
293-
value: 'cnn',
294-
child: Text(l10n.headlinesFeedFilterSourceCNN),
295-
),
296-
DropdownMenuItem(
297-
value: 'reuters',
298-
child: Text(l10n.headlinesFeedFilterSourceReuters),
279+
280+
// --- Source Filters ---
281+
Text(
282+
l10n.headlinesFeedFilterSourceLabel,
283+
style: Theme.of(context).textTheme.titleMedium,
284+
),
285+
const SizedBox(height: AppSpacing.md),
286+
// TODO(cline): Implement multi-select UI for sources
287+
// Fetch available sources from a repository
288+
// Use Wrap + FilterChip to display options
289+
// Update selectedSources list in setState when chips are toggled
290+
// Example placeholder:
291+
Container(
292+
padding: const EdgeInsets.all(AppSpacing.md),
293+
decoration: BoxDecoration(
294+
border: Border.all(
295+
color: Theme.of(context).colorScheme.outline,
299296
),
300-
],
301-
onChanged: (value) {
302-
setState(() {
303-
selectedSource = value;
304-
});
305-
},
297+
borderRadius: BorderRadius.circular(AppSpacing.sm),
298+
),
299+
child: const Text('Source FilterChip UI Placeholder'),
306300
),
307301
const SizedBox(height: AppSpacing.lg),
308-
// Event Country Dropdown
309-
DropdownButtonFormField<String>(
310-
decoration: InputDecoration(
311-
labelText: l10n.headlinesFeedFilterEventCountryLabel,
312-
),
313-
value: selectedEventCountry,
314-
// TODO(fulleni): Populate items dynamically
315-
items: [
316-
DropdownMenuItem<String>(
317-
child: Text(l10n.headlinesFeedFilterAllOption),
318-
),
319-
DropdownMenuItem(
320-
value: 'US',
321-
child: Text(l10n.headlinesFeedFilterCountryUS),
322-
),
323-
DropdownMenuItem(
324-
value: 'UK',
325-
child: Text(l10n.headlinesFeedFilterCountryUK),
326-
),
327-
DropdownMenuItem(
328-
value: 'CA',
329-
child: Text(l10n.headlinesFeedFilterCountryCA),
302+
303+
// --- Event Country Filters ---
304+
Text(
305+
l10n.headlinesFeedFilterEventCountryLabel,
306+
style: Theme.of(context).textTheme.titleMedium,
307+
),
308+
const SizedBox(height: AppSpacing.md),
309+
// TODO(cline): Implement multi-select UI for event countries
310+
// Fetch available countries from a repository
311+
// Use Wrap + FilterChip to display options
312+
// Update selectedEventCountries list in setState when chips are toggled
313+
// Example placeholder:
314+
Container(
315+
padding: const EdgeInsets.all(AppSpacing.md),
316+
decoration: BoxDecoration(
317+
border: Border.all(
318+
color: Theme.of(context).colorScheme.outline,
330319
),
331-
],
332-
onChanged: (value) {
333-
setState(() {
334-
selectedEventCountry = value;
335-
});
336-
},
320+
borderRadius: BorderRadius.circular(AppSpacing.sm),
321+
),
322+
child: const Text('Country FilterChip UI Placeholder'),
337323
),
338324
const SizedBox(height: AppSpacing.xl),
339-
// Use FilledButton for primary action
325+
326+
// --- Action Buttons ---
340327
FilledButton(
341328
onPressed: () {
342329
widget.bloc.add(
343330
HeadlinesFeedFilterChanged(
344-
// Pass null if 'All' was selected (value is null now)
345-
category: selectedCategory,
346-
source: selectedSource,
347-
eventCountry: selectedEventCountry,
331+
// Pass the selected lists
332+
categories:
333+
selectedCategories.isNotEmpty
334+
? selectedCategories
335+
: null,
336+
sources:
337+
selectedSources.isNotEmpty ? selectedSources : null,
338+
eventCountries:
339+
selectedEventCountries.isNotEmpty
340+
? selectedEventCountries
341+
: null,
348342
),
349343
);
350344
Navigator.pop(context);
@@ -354,12 +348,20 @@ class _HeadlinesFilterBottomSheetState
354348
const SizedBox(height: AppSpacing.sm),
355349
TextButton(
356350
onPressed: () {
351+
// Clear local state lists
357352
setState(() {
358-
selectedCategory = null;
359-
selectedSource = null;
360-
selectedEventCountry = null;
353+
selectedCategories.clear();
354+
selectedSources.clear();
355+
selectedEventCountries.clear();
361356
});
362-
widget.bloc.add(const HeadlinesFeedFilterChanged());
357+
// Dispatch event with null lists to clear filters in BLoC
358+
widget.bloc.add(
359+
const HeadlinesFeedFilterChanged(
360+
categories: null,
361+
sources: null,
362+
eventCountries: null,
363+
),
364+
);
363365
Navigator.pop(context);
364366
},
365367
child: Text(l10n.headlinesFeedFilterResetButton),

0 commit comments

Comments
 (0)