feat: Implement agent filtering by integrating a new API endpoint for filterable fields, introducing filter data models, and adding a dedicated filter sheet UI, alongside an updated profile icon.

This commit is contained in:
pradeepkumar
2026-03-07 22:33:09 +05:30
parent 402c1cf206
commit ef0fe593ab
7 changed files with 568 additions and 19 deletions

View File

@@ -1,5 +1,3 @@
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="11" stroke="#E58625" stroke-width="1.5"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M15 27.2635C17.1396 27.2666 19.2419 26.7023 21.0938 25.6277V21.6228C21.0938 20.5007 20.6493 19.4247 19.8581 18.6313C19.0669 17.8379 17.9939 17.3922 16.875 17.3922H13.125C12.0061 17.3922 10.9331 17.8379 10.1419 18.6313C9.35072 19.4247 8.90625 20.5007 8.90625 21.6228V25.6277C10.7581 26.7023 12.8604 27.2666 15 27.2635ZM23.9062 21.6228V23.3845C25.5276 21.644 26.6064 19.4664 27.0101 17.1193C27.4139 14.7722 27.1249 12.3579 26.1787 10.1733C25.2326 7.98864 23.6705 6.12885 21.6845 4.82252C19.6985 3.51619 17.3751 2.82025 15 2.82025C12.6249 2.82025 10.3015 3.51619 8.31549 4.82252C6.32949 6.12885 4.7674 7.98864 3.82125 10.1733C2.8751 12.3579 2.58613 14.7722 2.98986 17.1193C3.39359 19.4664 4.47243 21.644 6.09375 23.3845V21.6228C6.09319 20.169 6.54077 18.7506 7.3752 17.5617C8.20962 16.3729 9.39011 15.4718 10.755 14.9817C10.047 14.1651 9.58815 13.1614 9.43325 12.0905C9.27835 11.0196 9.43392 9.92655 9.88139 8.94183C10.3289 7.9571 11.0494 7.12224 11.9569 6.53681C12.8645 5.95138 13.9209 5.64011 15 5.64011C16.0791 5.64011 17.1355 5.95138 18.0431 6.53681C18.9506 7.12224 19.6711 7.9571 20.1186 8.94183C20.5661 9.92655 20.7216 11.0196 20.5667 12.0905C20.4118 13.1614 19.953 14.1651 19.245 14.9817C20.6099 15.4718 21.7904 16.3729 22.6248 17.5617C23.4592 18.7506 23.9068 20.169 23.9062 21.6228ZM15 30.0838C18.9782 30.0838 22.7936 28.4991 25.6066 25.6782C28.4196 22.8573 30 19.0313 30 15.0419C30 11.0526 28.4196 7.22658 25.6066 4.40567C22.7936 1.58477 18.9782 0 15 0C11.0218 0 7.20644 1.58477 4.3934 4.40567C1.58035 7.22658 0 11.0526 0 15.0419C0 19.0313 1.58035 22.8573 4.3934 25.6782C7.20644 28.4991 11.0218 30.0838 15 30.0838ZM17.8125 11.2814C17.8125 12.0294 17.5162 12.7468 16.9887 13.2757C16.4613 13.8047 15.7459 14.1018 15 14.1018C14.2541 14.1018 13.5387 13.8047 13.0113 13.2757C12.4838 12.7468 12.1875 12.0294 12.1875 11.2814C12.1875 10.5334 12.4838 9.81606 13.0113 9.28714C13.5387 8.75822 14.2541 8.46108 15 8.46108C15.7459 8.46108 16.4613 8.75822 16.9887 9.28714C17.5162 9.81606 17.8125 10.5334 17.8125 11.2814Z" fill="#E58625"/>
<circle cx="12" cy="9" r="3.5" stroke="#E58625" stroke-width="1.5"/>
<path d="M5.5 19.5C5.5 16.5 8.5 14 12 14C15.5 14 18.5 16.5 18.5 19.5" stroke="#E58625" stroke-width="1.5" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -16,6 +16,9 @@ class ApiConstants {
static const String agents = '/agents'; static const String agents = '/agents';
static const String agentTypes = '/agent-types'; static const String agentTypes = '/agent-types';
// Profile fields
static const String filterableFields = '/profile-fields/filterable';
// CMS // CMS
static const String cmsPageLanding = '/cms/page/landing'; static const String cmsPageLanding = '/cms/page/landing';
} }

View File

@@ -1,9 +1,12 @@
import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:real_estate_mobile/core/constants/api_constants.dart'; import 'package:real_estate_mobile/core/constants/api_constants.dart';
import 'package:real_estate_mobile/core/network/api_client.dart'; import 'package:real_estate_mobile/core/network/api_client.dart';
import 'package:real_estate_mobile/core/network/api_exceptions.dart'; import 'package:real_estate_mobile/core/network/api_exceptions.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
import 'package:real_estate_mobile/features/agents/data/models/filterable_field.dart';
class SearchAgentsResponse { class SearchAgentsResponse {
final List<AgentProfile> data; final List<AgentProfile> data;
@@ -49,6 +52,7 @@ class AgentsRepository {
int limit = 10, int limit = 10,
String? sortBy, String? sortBy,
String? sortOrder, String? sortOrder,
Map<String, List<String>>? filters,
}) async { }) async {
try { try {
final queryParams = <String, dynamic>{ final queryParams = <String, dynamic>{
@@ -60,6 +64,9 @@ class AgentsRepository {
if (search != null && search.isNotEmpty) queryParams['search'] = search; if (search != null && search.isNotEmpty) queryParams['search'] = search;
if (sortBy != null) queryParams['sortBy'] = sortBy; if (sortBy != null) queryParams['sortBy'] = sortBy;
if (sortOrder != null) queryParams['sortOrder'] = sortOrder; if (sortOrder != null) queryParams['sortOrder'] = sortOrder;
if (filters != null && filters.isNotEmpty) {
queryParams['filters'] = jsonEncode(filters);
}
final response = await _dio.get( final response = await _dio.get(
ApiConstants.agents, ApiConstants.agents,
@@ -81,6 +88,26 @@ class AgentsRepository {
} }
} }
/// Get filterable profile fields: GET /profile-fields/filterable
/// Response: { success, data: [...] }
Future<List<FilterableField>> getFilterableFields() async {
try {
final response = await _dio.get(ApiConstants.filterableFields);
final data = response.data['data'] as List<dynamic>;
return data
.map((e) => FilterableField.fromJson(e as Map<String, dynamic>))
.toList();
} on DioException catch (e) {
if (e.error is ApiException) {
throw e.error as ApiException;
}
throw ApiException(
message: e.message ?? 'Failed to fetch filterable fields',
statusCode: e.response?.statusCode,
);
}
}
/// Get agent types: GET /agent-types /// Get agent types: GET /agent-types
/// Response: { success, data: [...] } /// Response: { success, data: [...] }
Future<List<AgentType>> getAgentTypes() async { Future<List<AgentType>> getAgentTypes() async {

View File

@@ -0,0 +1,39 @@
class FilterOption {
final String value;
final String label;
const FilterOption({required this.value, required this.label});
factory FilterOption.fromJson(Map<String, dynamic> json) {
return FilterOption(
value: json['value'] as String? ?? '',
label: json['label'] as String? ?? '',
);
}
}
class FilterableField {
final String slug;
final String name;
final String fieldType;
final List<FilterOption> options;
const FilterableField({
required this.slug,
required this.name,
required this.fieldType,
this.options = const [],
});
factory FilterableField.fromJson(Map<String, dynamic> json) {
return FilterableField(
slug: json['slug'] as String? ?? '',
name: json['name'] as String? ?? '',
fieldType: json['fieldType'] as String? ?? '',
options: (json['options'] as List<dynamic>?)
?.map((e) => FilterOption.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
);
}
}

View File

@@ -2,11 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:real_estate_mobile/features/agents/data/agents_repository.dart'; import 'package:real_estate_mobile/features/agents/data/agents_repository.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
import 'package:real_estate_mobile/features/agents/data/models/filterable_field.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart';
class SearchAgentsState { class SearchAgentsState {
final List<AgentProfile> agents; final List<AgentProfile> agents;
final List<AgentType> agentTypes; final List<AgentType> agentTypes;
final List<FilterableField> filterFields;
final bool isLoading; final bool isLoading;
final bool isLoadingMore; final bool isLoadingMore;
final String? error; final String? error;
@@ -14,10 +16,12 @@ class SearchAgentsState {
final String? selectedAgentTypeId; final String? selectedAgentTypeId;
final int currentPage; final int currentPage;
final int totalPages; final int totalPages;
final Map<String, List<String>> activeFilters;
const SearchAgentsState({ const SearchAgentsState({
this.agents = const [], this.agents = const [],
this.agentTypes = const [], this.agentTypes = const [],
this.filterFields = const [],
this.isLoading = false, this.isLoading = false,
this.isLoadingMore = false, this.isLoadingMore = false,
this.error, this.error,
@@ -25,6 +29,7 @@ class SearchAgentsState {
this.selectedAgentTypeId, this.selectedAgentTypeId,
this.currentPage = 1, this.currentPage = 1,
this.totalPages = 1, this.totalPages = 1,
this.activeFilters = const {},
}); });
bool get hasMore => currentPage < totalPages; bool get hasMore => currentPage < totalPages;
@@ -32,6 +37,7 @@ class SearchAgentsState {
SearchAgentsState copyWith({ SearchAgentsState copyWith({
List<AgentProfile>? agents, List<AgentProfile>? agents,
List<AgentType>? agentTypes, List<AgentType>? agentTypes,
List<FilterableField>? filterFields,
bool? isLoading, bool? isLoading,
bool? isLoadingMore, bool? isLoadingMore,
String? error, String? error,
@@ -40,10 +46,12 @@ class SearchAgentsState {
bool clearAgentTypeId = false, bool clearAgentTypeId = false,
int? currentPage, int? currentPage,
int? totalPages, int? totalPages,
Map<String, List<String>>? activeFilters,
}) { }) {
return SearchAgentsState( return SearchAgentsState(
agents: agents ?? this.agents, agents: agents ?? this.agents,
agentTypes: agentTypes ?? this.agentTypes, agentTypes: agentTypes ?? this.agentTypes,
filterFields: filterFields ?? this.filterFields,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore, isLoadingMore: isLoadingMore ?? this.isLoadingMore,
error: error, error: error,
@@ -53,6 +61,7 @@ class SearchAgentsState {
: (selectedAgentTypeId ?? this.selectedAgentTypeId), : (selectedAgentTypeId ?? this.selectedAgentTypeId),
currentPage: currentPage ?? this.currentPage, currentPage: currentPage ?? this.currentPage,
totalPages: totalPages ?? this.totalPages, totalPages: totalPages ?? this.totalPages,
activeFilters: activeFilters ?? this.activeFilters,
); );
} }
} }
@@ -67,8 +76,14 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
Future<void> _init() async { Future<void> _init() async {
try { try {
final agentTypes = await _repository.getAgentTypes(); final results = await Future.wait([
state = state.copyWith(agentTypes: agentTypes); _repository.getAgentTypes(),
_repository.getFilterableFields(),
]);
state = state.copyWith(
agentTypes: results[0] as List<AgentType>,
filterFields: results[1] as List<FilterableField>,
);
await _fetchAgents(page: 1); await _fetchAgents(page: 1);
} catch (e) { } catch (e) {
state = state.copyWith(isLoading: false, error: e.toString()); state = state.copyWith(isLoading: false, error: e.toString());
@@ -82,6 +97,7 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
agentTypeId: state.selectedAgentTypeId, agentTypeId: state.selectedAgentTypeId,
page: page, page: page,
limit: 10, limit: 10,
filters: state.activeFilters.isNotEmpty ? state.activeFilters : null,
); );
state = state.copyWith( state = state.copyWith(
@@ -119,6 +135,24 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
_fetchAgents(page: 1); _fetchAgents(page: 1);
} }
void applyFilters(Map<String, List<String>> filters) {
state = state.copyWith(
activeFilters: filters,
isLoading: true,
currentPage: 1,
);
_fetchAgents(page: 1);
}
void clearFilters() {
state = state.copyWith(
activeFilters: const {},
isLoading: true,
currentPage: 1,
);
_fetchAgents(page: 1);
}
void loadMore() { void loadMore() {
if (state.isLoadingMore || !state.hasMore) return; if (state.isLoadingMore || !state.hasMore) return;
state = state.copyWith(isLoadingMore: true); state = state.copyWith(isLoadingMore: true);

View File

@@ -7,6 +7,7 @@ import 'package:real_estate_mobile/core/widgets/s3_image.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/search_agents_provider.dart';
import 'package:real_estate_mobile/features/agents/presentation/widgets/agent_filter_sheet.dart';
import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart'; import 'package:real_estate_mobile/features/home/presentation/widgets/home_header.dart';
class AgentSearchScreen extends ConsumerStatefulWidget { class AgentSearchScreen extends ConsumerStatefulWidget {
@@ -52,6 +53,27 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
} }
} }
void _openFilterSheet() {
final state = ref.read(searchAgentsProvider);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => AgentFilterSheet(
filterFields: state.filterFields,
agentTypes: state.agentTypes,
currentFilters: state.activeFilters,
selectedAgentTypeId: state.selectedAgentTypeId,
onApply: (filters) {
ref.read(searchAgentsProvider.notifier).applyFilters(filters);
},
onClearAll: () {
ref.read(searchAgentsProvider.notifier).clearFilters();
},
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = ref.watch(searchAgentsProvider); final state = ref.watch(searchAgentsProvider);
@@ -222,6 +244,8 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
Padding( Padding(
padding: const EdgeInsets.only(right: 24), padding: const EdgeInsets.only(right: 24),
child: GestureDetector(
onTap: () => _openFilterSheet(),
child: Container( child: Container(
width: 39, width: 39,
height: 27, height: 27,
@@ -239,6 +263,7 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
), ),
), ),
), ),
),
], ],
), ),
), ),

View File

@@ -0,0 +1,423 @@
import 'package:flutter/material.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
import 'package:real_estate_mobile/features/agents/data/models/filterable_field.dart';
class AgentFilterSheet extends StatefulWidget {
final List<FilterableField> filterFields;
final List<AgentType> agentTypes;
final Map<String, List<String>> currentFilters;
final String? selectedAgentTypeId;
final void Function(Map<String, List<String>> filters) onApply;
final VoidCallback onClearAll;
const AgentFilterSheet({
super.key,
required this.filterFields,
required this.agentTypes,
required this.currentFilters,
this.selectedAgentTypeId,
required this.onApply,
required this.onClearAll,
});
@override
State<AgentFilterSheet> createState() => _AgentFilterSheetState();
}
class _AgentFilterSheetState extends State<AgentFilterSheet> {
late Map<String, List<String>> _filters;
final Map<String, bool> _expandedSections = {};
@override
void initState() {
super.initState();
// Deep copy current filters
_filters = {};
for (final entry in widget.currentFilters.entries) {
_filters[entry.key] = List<String>.from(entry.value);
}
// Expand all sections by default
for (final field in widget.filterFields) {
_expandedSections[field.slug] = true;
}
}
void _toggleOption(String fieldSlug, String optionValue) {
setState(() {
final current = _filters[fieldSlug] ?? [];
if (current.contains(optionValue)) {
current.remove(optionValue);
} else {
current.add(optionValue);
}
_filters[fieldSlug] = current;
});
}
bool _isSelected(String fieldSlug, String optionValue) {
return _filters[fieldSlug]?.contains(optionValue) ?? false;
}
void _clearAll() {
setState(() {
_filters.clear();
});
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.85,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// Handle bar
Center(
child: Container(
margin: const EdgeInsets.only(top: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
),
// Header
Padding(
padding: const EdgeInsets.fromLTRB(27, 16, 27, 12),
child: Row(
children: [
const Text(
'Filters',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: const Icon(
Icons.close,
color: AppColors.primaryDark,
size: 24,
),
),
],
),
),
const Divider(height: 1, color: Color(0xFFE8E8E8)),
// Listing Type row
Padding(
padding: const EdgeInsets.fromLTRB(27, 16, 27, 12),
child: Row(
children: [
const Text(
'Listing Type',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
const Spacer(),
..._buildListingTypeChips(),
],
),
),
// Scrollable filter sections
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 27),
itemCount: widget.filterFields.length,
itemBuilder: (context, index) {
return _buildFilterSection(widget.filterFields[index]);
},
),
),
// Footer: divider + Clear All / Apply buttons
const Divider(height: 1, color: Color(0xFFE8E8E8)),
Padding(
padding: const EdgeInsets.fromLTRB(27, 16, 27, 16),
child: SafeArea(
top: false,
child: Row(
children: [
// Clear All
Expanded(
child: GestureDetector(
onTap: () {
_clearAll();
widget.onClearAll();
Navigator.of(context).pop();
},
child: Container(
height: 29,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark,
width: 0.1,
),
),
child: const Center(
child: Text(
'Clear All',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
),
),
),
const SizedBox(width: 16),
// Apply
Expanded(
child: GestureDetector(
onTap: () {
// Remove empty filter entries
final cleanFilters = <String, List<String>>{};
for (final entry in _filters.entries) {
if (entry.value.isNotEmpty) {
cleanFilters[entry.key] = entry.value;
}
}
widget.onApply(cleanFilters);
Navigator.of(context).pop();
},
child: Container(
height: 29,
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
child: const Center(
child: Text(
'Apply',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
),
),
),
],
),
),
),
],
),
);
}
List<Widget> _buildListingTypeChips() {
// Find "Professional" and "Lender" agent types
final types = <_ListingChip>[];
for (final at in widget.agentTypes) {
final name = at.name.toLowerCase();
if (name == 'professional' || name == 'agent') {
types.add(_ListingChip(label: 'Agents', id: at.id));
} else if (name == 'lender') {
types.add(_ListingChip(label: 'Lenders', id: at.id));
}
}
if (types.isEmpty) {
types.add(const _ListingChip(label: 'Agents', id: null));
types.add(const _ListingChip(label: 'Lenders', id: null));
}
return types.map((chip) {
final isSelected = chip.id == widget.selectedAgentTypeId;
return Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
chip.label,
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 18,
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
color: AppColors.primaryDark,
),
),
);
}).toList();
}
Widget _buildFilterSection(FilterableField field) {
final isExpanded = _expandedSections[field.slug] ?? true;
final options = field.options;
if (options.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
// Section header with expand/collapse
GestureDetector(
onTap: () {
setState(() {
_expandedSections[field.slug] = !isExpanded;
});
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Expanded(
child: Text(
field.name,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.primaryDark,
),
),
),
Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color: AppColors.primaryDark,
size: 20,
),
],
),
),
),
// Options list
if (isExpanded)
_FilterOptionsList(
field: field,
isSelected: (value) => _isSelected(field.slug, value),
onToggle: (value) => _toggleOption(field.slug, value),
),
const SizedBox(height: 4),
],
);
}
}
class _ListingChip {
final String label;
final String? id;
const _ListingChip({required this.label, required this.id});
}
class _FilterOptionsList extends StatefulWidget {
final FilterableField field;
final bool Function(String value) isSelected;
final void Function(String value) onToggle;
const _FilterOptionsList({
required this.field,
required this.isSelected,
required this.onToggle,
});
@override
State<_FilterOptionsList> createState() => _FilterOptionsListState();
}
class _FilterOptionsListState extends State<_FilterOptionsList> {
bool _showAll = false;
static const _initialCount = 6;
@override
Widget build(BuildContext context) {
final options = widget.field.options;
final displayOptions =
_showAll ? options : options.take(_initialCount).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...displayOptions.map((option) => _buildCheckboxRow(option)),
if (options.length > _initialCount)
GestureDetector(
onTap: () => setState(() => _showAll = !_showAll),
child: Padding(
padding: const EdgeInsets.only(left: 27, top: 8),
child: Text(
_showAll ? 'Show Less' : 'Show More',
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
),
),
],
);
}
Widget _buildCheckboxRow(FilterOption option) {
final selected = widget.isSelected(option.value);
return GestureDetector(
onTap: () => widget.onToggle(option.value),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
Container(
width: 17,
height: 17,
decoration: BoxDecoration(
color: selected ? AppColors.accentOrange : Colors.transparent,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: selected
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.2),
width: 0.2,
),
),
child: selected
? const Icon(Icons.check, size: 13, color: Colors.white)
: null,
),
const SizedBox(width: 10),
Expanded(
child: Text(
option.label,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 15,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
],
),
),
);
}
}