Skip to content

Commit 1a3e386

Browse files
committed
feat(search): implement headlines search page
- Added search bloc and events - Implemented search UI - Integrated with headlines repository - Added debounce for search term changes
1 parent ffbc015 commit 1a3e386

File tree

8 files changed

+187
-7
lines changed

8 files changed

+187
-7
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:ht_headlines_repository/ht_headlines_repository.dart';
4+
import 'package:stream_transform/stream_transform.dart';
5+
6+
part 'headlines_search_event.dart';
7+
part 'headlines_search_state.dart';
8+
9+
class HeadlinesSearchBloc
10+
extends Bloc<HeadlinesSearchEvent, HeadlinesSearchState> {
11+
HeadlinesSearchBloc({required HtHeadlinesRepository headlinesRepository})
12+
: _headlinesRepository = headlinesRepository,
13+
super(HeadlinesSearchInitial()) {
14+
on<HeadlinesSearchTermChanged>(
15+
_onSearchTermChanged,
16+
transformer: (events, mapper) => events
17+
.debounce(const Duration(milliseconds: 300))
18+
.asyncExpand(mapper),
19+
);
20+
on<HeadlinesSearchRequested>(_onSearchRequested);
21+
}
22+
23+
final HtHeadlinesRepository _headlinesRepository;
24+
String _searchTerm = '';
25+
26+
Future<void> _onSearchTermChanged(
27+
HeadlinesSearchTermChanged event,
28+
Emitter<HeadlinesSearchState> emit,
29+
) async {
30+
_searchTerm = event.searchTerm;
31+
if (_searchTerm.isEmpty) {
32+
emit(HeadlinesSearchInitial());
33+
}
34+
}
35+
36+
Future<void> _onSearchRequested(
37+
HeadlinesSearchRequested event,
38+
Emitter<HeadlinesSearchState> emit,
39+
) async {
40+
if (_searchTerm.isEmpty) {
41+
return; // Don't search if the term is empty
42+
}
43+
emit(HeadlinesSearchLoading());
44+
try {
45+
final response =
46+
await _headlinesRepository.searchHeadlines(query: _searchTerm);
47+
emit(HeadlinesSearchLoaded(headlines: response.items));
48+
} on HeadlinesSearchException catch (e) {
49+
emit(HeadlinesSearchError(message: e.message));
50+
} catch (e) {
51+
emit(HeadlinesSearchError(message: e.toString()));
52+
}
53+
}
54+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
part of 'headlines_search_bloc.dart';
2+
3+
sealed class HeadlinesSearchEvent extends Equatable {
4+
const HeadlinesSearchEvent();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
final class HeadlinesSearchTermChanged extends HeadlinesSearchEvent {
11+
const HeadlinesSearchTermChanged({required this.searchTerm});
12+
13+
final String searchTerm;
14+
15+
@override
16+
List<Object> get props => [searchTerm];
17+
}
18+
19+
final class HeadlinesSearchRequested extends HeadlinesSearchEvent {}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
part of 'headlines_search_bloc.dart';
2+
3+
sealed class HeadlinesSearchState extends Equatable {
4+
const HeadlinesSearchState();
5+
6+
@override
7+
List<Object> get props => [];
8+
}
9+
10+
final class HeadlinesSearchInitial extends HeadlinesSearchState {}
11+
12+
final class HeadlinesSearchLoading extends HeadlinesSearchState {}
13+
14+
final class HeadlinesSearchLoaded extends HeadlinesSearchState {
15+
const HeadlinesSearchLoaded({required this.headlines});
16+
17+
final List<Headline> headlines;
18+
19+
@override
20+
List<Object> get props => [headlines];
21+
}
22+
23+
final class HeadlinesSearchError extends HeadlinesSearchState {
24+
const HeadlinesSearchError({required this.message});
25+
26+
final String message;
27+
28+
@override
29+
List<Object> get props => [message];
30+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:ht_headlines_repository/ht_headlines_repository.dart';
4+
import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart';
5+
import 'package:ht_main/headlines-search/view/headlines_search_view.dart';
6+
7+
class HeadlinesSearchPage extends StatelessWidget {
8+
const HeadlinesSearchPage({super.key});
9+
10+
static Route<void> route() {
11+
return MaterialPageRoute<void>(builder: (_) => const HeadlinesSearchPage());
12+
}
13+
14+
@override
15+
Widget build(BuildContext context) {
16+
return BlocProvider(
17+
create: (_) => HeadlinesSearchBloc(
18+
headlinesRepository: context.read<HtHeadlinesRepository>(),
19+
),
20+
child: const HeadlinesSearchView(),
21+
);
22+
}
23+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart';
4+
import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart';
5+
6+
class HeadlinesSearchView extends StatelessWidget {
7+
const HeadlinesSearchView({super.key});
8+
9+
@override
10+
Widget build(BuildContext context) {
11+
return Scaffold(
12+
appBar: AppBar(
13+
title: SearchBar(
14+
hintText: 'Search Headlines',
15+
onChanged: (value) {
16+
context
17+
.read<HeadlinesSearchBloc>()
18+
.add(HeadlinesSearchTermChanged(searchTerm: value));
19+
},
20+
onSubmitted: (value) {
21+
context.read<HeadlinesSearchBloc>().add(HeadlinesSearchRequested());
22+
},
23+
),
24+
),
25+
body: BlocBuilder<HeadlinesSearchBloc, HeadlinesSearchState>(
26+
builder: (context, state) {
27+
if (state is HeadlinesSearchInitial) {
28+
return const Center(
29+
child: Column(
30+
mainAxisAlignment: MainAxisAlignment.center,
31+
children: [
32+
Icon(Icons.search, size: 64),
33+
SizedBox(height: 16),
34+
Text('Search Headlines', style: TextStyle(fontSize: 24)),
35+
Text('Enter keywords to find articles'),
36+
],
37+
),
38+
);
39+
} else if (state is HeadlinesSearchLoading) {
40+
return const Center(child: CircularProgressIndicator());
41+
} else if (state is HeadlinesSearchLoaded) {
42+
return ListView.builder(
43+
itemCount: state.headlines.length,
44+
itemBuilder: (context, index) {
45+
return HeadlineItemWidget(headline: state.headlines[index]);
46+
},
47+
);
48+
} else if (state is HeadlinesSearchError) {
49+
return Center(child: Text(state.message));
50+
}
51+
return Container(); // Should never reach here
52+
},
53+
),
54+
);
55+
}
56+
}

lib/router/router.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
33
import 'package:ht_main/app/view/app_scaffold.dart';
44
import 'package:ht_main/headlines-feed/view/headlines_feed_page.dart';
5+
import 'package:ht_main/headlines-search/view/headlines_search_page.dart';
56
import 'package:ht_main/router/routes.dart';
67

78
final appRouter = GoRouter(
@@ -33,11 +34,7 @@ final appRouter = GoRouter(
3334
path: Routes.search,
3435
name: Routes.searchName,
3536
builder: (BuildContext context, GoRouterState state) {
36-
return const Placeholder(
37-
child: Center(
38-
child: Text('SEARCH PAGE'),
39-
),
40-
);
37+
return const HeadlinesSearchPage();
4138
},
4239
),
4340
GoRoute(

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ packages:
607607
source: hosted
608608
version: "2.1.4"
609609
stream_transform:
610-
dependency: transitive
610+
dependency: "direct main"
611611
description:
612612
name: stream_transform
613613
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871

pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: ht_main
22
description: main headlines toolkit mobile app.
3-
version: 0.14.0
3+
version: 0.15.0
44
publish_to: none
55
repository: https://github.com/headlines-toolkit/ht-main
66
environment:
@@ -33,6 +33,7 @@ dependencies:
3333
ref: main
3434
intl: ^0.19.0
3535
meta: ^1.16.0
36+
stream_transform: ^2.1.1
3637

3738
dev_dependencies:
3839
bloc_test: ^10.0.0

0 commit comments

Comments
 (0)