Skip to content

Commit f936983

Browse files
committed
feat: implement search screen
1 parent 333ed3d commit f936983

15 files changed

Lines changed: 540 additions & 49 deletions

File tree

lib/data/constants/strings.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class AppStrings {
44
static const String authTokenKey = 'auth_token';
55
static const String userKey = 'user';
66
static const String filterKey = 'filter';
7+
static const String recentSearchKey = 'recent_search';
78

89
// Error
910
static const String genericError = 'Something went wrong';

lib/data/services/local/storage_service.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ class StorageService {
7777
}
7878
}
7979

80+
/// recent searches
81+
static List<User>? get recentSearches {
82+
try {
83+
return List<User>.from(json
84+
.decode(_get<String>(AppStrings.recentSearchKey)!)
85+
.map((e) => User.fromJson(e)));
86+
} on Exception catch (_) {
87+
return null;
88+
}
89+
}
90+
91+
static set recentSearches(List<User>? users) {
92+
try {
93+
_set<String>(AppStrings.recentSearchKey,
94+
json.encode(users!.map((user) => user.toJson()).toList()));
95+
} on Exception catch (_) {
96+
_set<String?>(AppStrings.recentSearchKey, null);
97+
}
98+
}
99+
80100
static set filter(ContestFilter? _filter) =>
81101
_set(AppStrings.filterKey, json.encode(_filter!.toJson()));
82102
}

lib/domain/models/user.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ part 'user.g.dart';
1111
class User with _$User {
1212
factory User({
1313
required String fullname,
14-
required String email,
14+
String? email,
1515
Handle? handle,
1616
String? id,
1717
String? institute,

lib/domain/repositories/user_repository.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ class UserRepository {
196196

197197
final _users = <User>[];
198198
if (response['status_code'] == 200) {
199-
for (final user in json.decode(response['data']) ?? []) {
199+
for (final user in response['data'] ?? []) {
200200
_users.add(User.fromJson(user));
201201
}
202202
}
@@ -216,9 +216,7 @@ class UserRepository {
216216
);
217217

218218
if (response['status_code'] == 200) {
219-
return List<String>.from(
220-
json.decode(response['data']).map((e) => e),
221-
);
219+
return List<String>.from(response['data']);
222220
}
223221

224222
return [];

lib/presentation/components/inputs/text_input.dart

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ class TextInput extends StatelessWidget {
1919
this.suffix,
2020
this.isFilled,
2121
this.fillColor,
22+
this.border,
23+
this.onSubmitted,
2224
Key? key,
2325
}) : assert(
2426
controller != null || onChanged != null,
@@ -31,13 +33,15 @@ class TextInput extends StatelessWidget {
3133
final String initialValue;
3234
final String hint;
3335
final Function(String)? onChanged;
36+
final Function(String)? onSubmitted;
3437
final TextInputAction action;
3538
final TextInputType? keyboard;
3639
final TextEditingController? controller;
3740
final Widget? prefix;
3841
final Widget? suffix;
3942
final bool? isFilled;
4043
final Color? fillColor;
44+
final InputBorder? border;
4145

4246
@override
4347
Widget build(BuildContext context) {
@@ -48,9 +52,12 @@ class TextInput extends StatelessWidget {
4852
hintStyle: AppStyles.h6,
4953
prefixIcon: prefix,
5054
suffixIcon: suffix,
51-
border: const OutlineInputBorder(
52-
borderSide: BorderSide(color: AppColors.primary),
53-
),
55+
border: border ??
56+
const OutlineInputBorder(
57+
borderSide: BorderSide(
58+
color: AppColors.primary,
59+
),
60+
),
5461
focusedBorder: const OutlineInputBorder(
5562
borderSide: BorderSide(
5663
color: AppColors.primary,
@@ -67,6 +74,7 @@ class TextInput extends StatelessWidget {
6774
onChanged: onChanged,
6875
style: AppStyles.h6.copyWith(color: AppColors.grey3),
6976
textInputAction: action,
77+
onFieldSubmitted: onSubmitted,
7078
);
7179
}
7280
}

lib/presentation/home/bloc/home_bloc.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
1919
late final List<Widget> screens;
2020

2121
void init() {
22-
screens = const <Widget>[
23-
FeedScreen(),
24-
ContestsScreen(),
22+
screens = <Widget>[
23+
const FeedScreen(),
24+
const ContestsScreen(),
2525
SearchScreen(),
26-
ProfileScreen(),
26+
const ProfileScreen(),
2727
];
2828
}
2929

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:freezed_annotation/freezed_annotation.dart';
4+
5+
import '../../../domain/models/user.dart';
6+
import '../../../domain/repositories/user_repository.dart';
7+
8+
part 'search_event.dart';
9+
part 'search_state.dart';
10+
part 'search_bloc.freezed.dart';
11+
12+
class SearchBloc extends Bloc<SearchEvent, SearchState> {
13+
SearchBloc() : super(const SearchState()) {
14+
on<FetchInstituteList>(_onFetchInstituteList);
15+
on<SearchPeople>(_onSearchPeople);
16+
on<UpdateFilterInstitute>(_onUpdateFilterInstitute);
17+
on<Reset>(_onReset);
18+
}
19+
20+
void _onFetchInstituteList(
21+
FetchInstituteList event,
22+
Emitter<SearchState> emit,
23+
) async {
24+
final res = await UserRepository.getInstituteList();
25+
if (res.isEmpty) {
26+
instituteList.addAll([
27+
'Indian Institute of Technology Roorkee',
28+
'Indian Institute of Technology Delhi',
29+
'Indian Institute of Technology Mandi',
30+
'Indian Institute of Technology Indore',
31+
'Indian Institute of Technology Bombay'
32+
]);
33+
} else {
34+
instituteList.addAll(res);
35+
}
36+
37+
emit(state.copyWith(instituteList: instituteList));
38+
}
39+
40+
void _onSearchPeople(SearchPeople event, Emitter<SearchState> emit) async {
41+
emit(state.copyWith(
42+
showSearches: true,
43+
isLoading: true,
44+
));
45+
46+
searchedResult = await UserRepository.search(event.query);
47+
applyFilter();
48+
49+
emit(state.copyWith(
50+
showSearches: true,
51+
isLoading: false,
52+
searchedResult: filteredSearchResult,
53+
));
54+
}
55+
56+
void _onUpdateFilterInstitute(
57+
UpdateFilterInstitute event,
58+
Emitter<SearchState> emit,
59+
) {
60+
applyFilter();
61+
emit(state.copyWith(
62+
selectedInstitute: updatedFilter,
63+
searchedResult: filteredSearchResult,
64+
));
65+
}
66+
67+
void applyFilter() {
68+
if (state.selectedInstitute == 'All') {
69+
filteredSearchResult = searchedResult;
70+
return;
71+
}
72+
73+
filteredSearchResult = [];
74+
filteredSearchResult.addAll(searchedResult.where(
75+
(element) => element.institute == state.selectedInstitute,
76+
));
77+
}
78+
79+
void _onReset(Reset event, Emitter<SearchState> emit) {
80+
emit(state.copyWith(
81+
showSearches: false,
82+
isLoading: false,
83+
));
84+
}
85+
86+
List<String> instituteList = ['All'];
87+
String updatedFilter = '';
88+
List<User> searchedResult = [];
89+
List<User> filteredSearchResult = [];
90+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
part of 'search_bloc.dart';
2+
3+
abstract class SearchEvent extends Equatable {
4+
const SearchEvent();
5+
6+
@override
7+
List<Object?> get props => [];
8+
}
9+
10+
class FetchInstituteList extends SearchEvent {
11+
const FetchInstituteList();
12+
}
13+
14+
class SearchPeople extends SearchEvent {
15+
const SearchPeople({required this.query});
16+
final String query;
17+
18+
@override
19+
List<Object?> get props => [query];
20+
}
21+
22+
class UpdateFilterInstitute extends SearchEvent {
23+
const UpdateFilterInstitute();
24+
}
25+
26+
class Reset extends SearchEvent {
27+
const Reset();
28+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
part of 'search_bloc.dart';
2+
3+
@freezed
4+
class SearchState with _$SearchState {
5+
const factory SearchState({
6+
@Default(true) bool isLoading,
7+
@Default([]) List<String> instituteList,
8+
@Default('All') String selectedInstitute,
9+
@Default('') String query,
10+
@Default(false) bool showSearches,
11+
@Default([]) List<User> searchedResult,
12+
@Default([]) List<User> recentSearches,
13+
}) = _SearchState;
14+
15+
const SearchState._();
16+
}
Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,94 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
23
import 'package:flutter_screenutil/flutter_screenutil.dart';
34

45
import '../../data/constants/colors.dart';
56
import '../components/inputs/text_input.dart';
7+
import 'bloc/search_bloc.dart';
8+
import 'widgets/filter_search.dart';
9+
import 'widgets/recent_searches.dart';
10+
import 'widgets/searched_result.dart';
611

712
class SearchScreen extends StatelessWidget {
8-
const SearchScreen({Key? key}) : super(key: key);
13+
SearchScreen({Key? key}) : super(key: key);
14+
final TextEditingController _controller = TextEditingController();
915

1016
@override
1117
Widget build(BuildContext context) {
12-
return Column(
13-
children: [
14-
Padding(
15-
padding: EdgeInsets.symmetric(
16-
horizontal: 13.w,
17-
vertical: 30.h,
18-
),
19-
child: TextInput(
20-
hint: 'Search people by name or handles',
21-
onChanged: (val) {},
22-
isFilled: true,
23-
fillColor: AppColors.grey7,
24-
suffix: Row(
25-
mainAxisSize: MainAxisSize.min,
26-
children: [
27-
IconButton(
28-
padding: const EdgeInsets.fromLTRB(4, 1, 1, 4),
29-
icon: const Icon(
30-
Icons.filter_alt,
31-
color: AppColors.grey6,
32-
),
33-
onPressed: () {},
18+
return BlocProvider<SearchBloc>(
19+
create: (context) => SearchBloc()..add(const FetchInstituteList()),
20+
child: BlocBuilder<SearchBloc, SearchState>(
21+
builder: (context, state) {
22+
return Column(
23+
children: [
24+
Padding(
25+
padding: EdgeInsets.symmetric(
26+
horizontal: 13.w,
27+
vertical: 30.h,
3428
),
35-
IconButton(
36-
padding: const EdgeInsets.fromLTRB(4, 1, 1, 4),
37-
icon: const Icon(
38-
Icons.clear,
39-
color: AppColors.grey6,
29+
child: TextInput(
30+
hint: 'Search people by name or handles',
31+
onChanged: (val) {
32+
if (val.isEmpty) {
33+
context.read<SearchBloc>().add(const Reset());
34+
}
35+
},
36+
controller: _controller,
37+
onSubmitted: (val) {
38+
final res = val.trim();
39+
if (res.isEmpty) return;
40+
context.read<SearchBloc>().add(SearchPeople(query: res));
41+
},
42+
action: TextInputAction.search,
43+
isFilled: true,
44+
border: InputBorder.none,
45+
fillColor: AppColors.grey7,
46+
suffix: Row(
47+
mainAxisSize: MainAxisSize.min,
48+
children: [
49+
IconButton(
50+
padding: const EdgeInsets.fromLTRB(4, 1, 1, 4),
51+
icon: const Icon(
52+
Icons.filter_alt,
53+
color: AppColors.grey6,
54+
),
55+
onPressed: () {
56+
showModalBottomSheet(
57+
context: context,
58+
builder: (_) {
59+
return FilterSearch(
60+
bloc: context.read<SearchBloc>(),
61+
);
62+
},
63+
);
64+
},
65+
),
66+
IconButton(
67+
padding: const EdgeInsets.fromLTRB(4, 1, 1, 4),
68+
icon: const Icon(
69+
Icons.close,
70+
color: AppColors.grey6,
71+
),
72+
onPressed: () {
73+
_controller.clear();
74+
if (state.showSearches) {
75+
context.read<SearchBloc>().add(const Reset());
76+
}
77+
},
78+
),
79+
],
4080
),
41-
onPressed: () {},
4281
),
43-
],
44-
),
45-
),
46-
),
47-
],
82+
),
83+
Expanded(
84+
child: state.showSearches
85+
? const SearchedResult()
86+
: const RecentSearches(),
87+
),
88+
],
89+
);
90+
},
91+
),
4892
);
4993
}
5094
}

0 commit comments

Comments
 (0)