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:
@@ -16,6 +16,9 @@ class ApiConstants {
|
||||
static const String agents = '/agents';
|
||||
static const String agentTypes = '/agent-types';
|
||||
|
||||
// Profile fields
|
||||
static const String filterableFields = '/profile-fields/filterable';
|
||||
|
||||
// CMS
|
||||
static const String cmsPageLanding = '/cms/page/landing';
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.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_exceptions.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/filterable_field.dart';
|
||||
|
||||
class SearchAgentsResponse {
|
||||
final List<AgentProfile> data;
|
||||
@@ -49,6 +52,7 @@ class AgentsRepository {
|
||||
int limit = 10,
|
||||
String? sortBy,
|
||||
String? sortOrder,
|
||||
Map<String, List<String>>? filters,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
@@ -60,6 +64,9 @@ class AgentsRepository {
|
||||
if (search != null && search.isNotEmpty) queryParams['search'] = search;
|
||||
if (sortBy != null) queryParams['sortBy'] = sortBy;
|
||||
if (sortOrder != null) queryParams['sortOrder'] = sortOrder;
|
||||
if (filters != null && filters.isNotEmpty) {
|
||||
queryParams['filters'] = jsonEncode(filters);
|
||||
}
|
||||
|
||||
final response = await _dio.get(
|
||||
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
|
||||
/// Response: { success, data: [...] }
|
||||
Future<List<AgentType>> getAgentTypes() async {
|
||||
|
||||
39
lib/features/agents/data/models/filterable_field.dart
Normal file
39
lib/features/agents/data/models/filterable_field.dart
Normal 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 [],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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/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/filterable_field.dart';
|
||||
import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart';
|
||||
|
||||
class SearchAgentsState {
|
||||
final List<AgentProfile> agents;
|
||||
final List<AgentType> agentTypes;
|
||||
final List<FilterableField> filterFields;
|
||||
final bool isLoading;
|
||||
final bool isLoadingMore;
|
||||
final String? error;
|
||||
@@ -14,10 +16,12 @@ class SearchAgentsState {
|
||||
final String? selectedAgentTypeId;
|
||||
final int currentPage;
|
||||
final int totalPages;
|
||||
final Map<String, List<String>> activeFilters;
|
||||
|
||||
const SearchAgentsState({
|
||||
this.agents = const [],
|
||||
this.agentTypes = const [],
|
||||
this.filterFields = const [],
|
||||
this.isLoading = false,
|
||||
this.isLoadingMore = false,
|
||||
this.error,
|
||||
@@ -25,6 +29,7 @@ class SearchAgentsState {
|
||||
this.selectedAgentTypeId,
|
||||
this.currentPage = 1,
|
||||
this.totalPages = 1,
|
||||
this.activeFilters = const {},
|
||||
});
|
||||
|
||||
bool get hasMore => currentPage < totalPages;
|
||||
@@ -32,6 +37,7 @@ class SearchAgentsState {
|
||||
SearchAgentsState copyWith({
|
||||
List<AgentProfile>? agents,
|
||||
List<AgentType>? agentTypes,
|
||||
List<FilterableField>? filterFields,
|
||||
bool? isLoading,
|
||||
bool? isLoadingMore,
|
||||
String? error,
|
||||
@@ -40,10 +46,12 @@ class SearchAgentsState {
|
||||
bool clearAgentTypeId = false,
|
||||
int? currentPage,
|
||||
int? totalPages,
|
||||
Map<String, List<String>>? activeFilters,
|
||||
}) {
|
||||
return SearchAgentsState(
|
||||
agents: agents ?? this.agents,
|
||||
agentTypes: agentTypes ?? this.agentTypes,
|
||||
filterFields: filterFields ?? this.filterFields,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
error: error,
|
||||
@@ -53,6 +61,7 @@ class SearchAgentsState {
|
||||
: (selectedAgentTypeId ?? this.selectedAgentTypeId),
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
activeFilters: activeFilters ?? this.activeFilters,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -67,8 +76,14 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
final agentTypes = await _repository.getAgentTypes();
|
||||
state = state.copyWith(agentTypes: agentTypes);
|
||||
final results = await Future.wait([
|
||||
_repository.getAgentTypes(),
|
||||
_repository.getFilterableFields(),
|
||||
]);
|
||||
state = state.copyWith(
|
||||
agentTypes: results[0] as List<AgentType>,
|
||||
filterFields: results[1] as List<FilterableField>,
|
||||
);
|
||||
await _fetchAgents(page: 1);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
@@ -82,6 +97,7 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
|
||||
agentTypeId: state.selectedAgentTypeId,
|
||||
page: page,
|
||||
limit: 10,
|
||||
filters: state.activeFilters.isNotEmpty ? state.activeFilters : null,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
@@ -119,6 +135,24 @@ class SearchAgentsNotifier extends StateNotifier<SearchAgentsState> {
|
||||
_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() {
|
||||
if (state.isLoadingMore || !state.hasMore) return;
|
||||
state = state.copyWith(isLoadingMore: true);
|
||||
|
||||
@@ -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_type.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';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(searchAgentsProvider);
|
||||
@@ -222,20 +244,23 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 24),
|
||||
child: Container(
|
||||
width: 39,
|
||||
height: 27,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.1,
|
||||
child: GestureDetector(
|
||||
onTap: () => _openFilterSheet(),
|
||||
child: Container(
|
||||
width: 39,
|
||||
height: 27,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryDark,
|
||||
width: 0.1,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.tune,
|
||||
color: AppColors.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.tune,
|
||||
color: AppColors.primaryDark,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
423
lib/features/agents/presentation/widgets/agent_filter_sheet.dart
Normal file
423
lib/features/agents/presentation/widgets/agent_filter_sheet.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user