fix product search/filter
This commit is contained in:
@@ -8,31 +8,31 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
/// Product Filters State
|
||||
class ProductFiltersState {
|
||||
final Set<String> productLines;
|
||||
final Set<String> spaces;
|
||||
final Set<String> sizes;
|
||||
final Set<String> surfaces;
|
||||
final Set<String> colors;
|
||||
final Set<String> brands;
|
||||
|
||||
const ProductFiltersState({
|
||||
this.productLines = const {},
|
||||
this.spaces = const {},
|
||||
this.sizes = const {},
|
||||
this.surfaces = const {},
|
||||
this.colors = const {},
|
||||
this.brands = const {},
|
||||
});
|
||||
|
||||
ProductFiltersState copyWith({
|
||||
Set<String>? productLines,
|
||||
Set<String>? spaces,
|
||||
Set<String>? sizes,
|
||||
Set<String>? surfaces,
|
||||
Set<String>? colors,
|
||||
Set<String>? brands,
|
||||
}) {
|
||||
return ProductFiltersState(
|
||||
productLines: productLines ?? this.productLines,
|
||||
spaces: spaces ?? this.spaces,
|
||||
sizes: sizes ?? this.sizes,
|
||||
surfaces: surfaces ?? this.surfaces,
|
||||
colors: colors ?? this.colors,
|
||||
brands: brands ?? this.brands,
|
||||
);
|
||||
}
|
||||
@@ -40,9 +40,9 @@ class ProductFiltersState {
|
||||
/// Get total filter count
|
||||
int get totalCount =>
|
||||
productLines.length +
|
||||
spaces.length +
|
||||
sizes.length +
|
||||
surfaces.length +
|
||||
colors.length +
|
||||
brands.length;
|
||||
|
||||
/// Check if any filters are active
|
||||
@@ -70,17 +70,6 @@ class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
||||
state = state.copyWith(productLines: newSet);
|
||||
}
|
||||
|
||||
/// Toggle space filter
|
||||
void toggleSpace(String value) {
|
||||
final newSet = Set<String>.from(state.spaces);
|
||||
if (newSet.contains(value)) {
|
||||
newSet.remove(value);
|
||||
} else {
|
||||
newSet.add(value);
|
||||
}
|
||||
state = state.copyWith(spaces: newSet);
|
||||
}
|
||||
|
||||
/// Toggle size filter
|
||||
void toggleSize(String value) {
|
||||
final newSet = Set<String>.from(state.sizes);
|
||||
@@ -103,6 +92,17 @@ class ProductFiltersNotifier extends Notifier<ProductFiltersState> {
|
||||
state = state.copyWith(surfaces: newSet);
|
||||
}
|
||||
|
||||
/// Toggle color filter
|
||||
void toggleColor(String value) {
|
||||
final newSet = Set<String>.from(state.colors);
|
||||
if (newSet.contains(value)) {
|
||||
newSet.remove(value);
|
||||
} else {
|
||||
newSet.add(value);
|
||||
}
|
||||
state = state.copyWith(colors: newSet);
|
||||
}
|
||||
|
||||
/// Toggle brand filter
|
||||
void toggleBrand(String value) {
|
||||
final newSet = Set<String>.from(state.brands);
|
||||
|
||||
@@ -14,9 +14,8 @@ import 'package:worker/features/products/data/repositories/products_repository_i
|
||||
import 'package:worker/features/products/domain/entities/product.dart';
|
||||
import 'package:worker/features/products/domain/repositories/products_repository.dart';
|
||||
import 'package:worker/features/products/domain/usecases/get_products.dart';
|
||||
import 'package:worker/features/products/domain/usecases/search_products.dart';
|
||||
import 'package:worker/features/products/domain/usecases/get_product_detail.dart';
|
||||
import 'package:worker/features/products/presentation/providers/selected_brand_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/product_filters_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/search_query_provider.dart';
|
||||
|
||||
part 'products_provider.g.dart';
|
||||
@@ -39,7 +38,9 @@ Future<ProductsRemoteDataSource> productsRemoteDataSource(Ref ref) async {
|
||||
@riverpod
|
||||
Future<ProductsRepository> productsRepository(Ref ref) async {
|
||||
final localDataSource = ref.watch(productsLocalDataSourceProvider);
|
||||
final remoteDataSource = await ref.watch(productsRemoteDataSourceProvider.future);
|
||||
final remoteDataSource = await ref.watch(
|
||||
productsRemoteDataSourceProvider.future,
|
||||
);
|
||||
return ProductsRepositoryImpl(
|
||||
localDataSource: localDataSource,
|
||||
remoteDataSource: remoteDataSource,
|
||||
@@ -70,53 +71,66 @@ class Products extends _$Products {
|
||||
|
||||
@override
|
||||
Future<List<Product>> build() async {
|
||||
// Reset pagination when dependencies change
|
||||
// IMPORTANT: This method is called automatically whenever any watched
|
||||
// provider changes (searchQueryProvider, productFiltersProvider, etc.)
|
||||
// This ensures pagination is ALWAYS reset when filters/search change.
|
||||
|
||||
// Reset pagination state
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
|
||||
// Watch dependencies
|
||||
final selectedBrand = ref.watch(selectedBrandProvider);
|
||||
// Watch dependencies (triggers rebuild when they change)
|
||||
final searchQuery = ref.watch(searchQueryProvider);
|
||||
final filters = ref.watch(productFiltersProvider);
|
||||
|
||||
// Get repository with injected data sources
|
||||
final repository = await ref.watch(productsRepositoryProvider.future);
|
||||
|
||||
// Fetch first page of products
|
||||
// Fetch first page of products using unified API
|
||||
List<Product> products;
|
||||
|
||||
if (searchQuery.isNotEmpty) {
|
||||
// Search takes precedence over brand filter
|
||||
final searchUseCase = SearchProducts(repository);
|
||||
products = await searchUseCase(searchQuery);
|
||||
// Build filter parameters from filter drawer
|
||||
final List<String>? itemGroups = filters.productLines.isNotEmpty
|
||||
? filters.productLines.toList()
|
||||
: null;
|
||||
|
||||
// If a brand is selected, filter search results by brand
|
||||
if (selectedBrand != 'all') {
|
||||
products = products
|
||||
.where((product) => product.brand == selectedBrand)
|
||||
.toList();
|
||||
}
|
||||
// Use brands from productFiltersProvider (shared by chips and drawer)
|
||||
final List<String>? brands = filters.brands.isNotEmpty
|
||||
? filters.brands.toList()
|
||||
: null;
|
||||
|
||||
// For search, we fetch all results at once, so no more pages
|
||||
_hasMore = false;
|
||||
} else {
|
||||
// No search query, fetch all products with pagination
|
||||
final getProductsUseCase = GetProducts(repository);
|
||||
products = await getProductsUseCase(
|
||||
limitStart: 0,
|
||||
limitPageLength: pageSize,
|
||||
);
|
||||
// Build item attributes from filter drawer (sizes, surfaces, colors)
|
||||
final List<Map<String, String>> itemAttributes = [];
|
||||
|
||||
// Filter by brand if not 'all'
|
||||
if (selectedBrand != 'all') {
|
||||
products = products
|
||||
.where((product) => product.brand == selectedBrand)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// If we got less than pageSize, there are no more products
|
||||
_hasMore = products.length >= pageSize;
|
||||
// Add size attributes
|
||||
for (final size in filters.sizes) {
|
||||
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
||||
}
|
||||
|
||||
// Add surface attributes
|
||||
for (final surface in filters.surfaces) {
|
||||
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
||||
}
|
||||
|
||||
// Add color attributes
|
||||
for (final color in filters.colors) {
|
||||
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
||||
}
|
||||
|
||||
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
|
||||
|
||||
// Use the comprehensive getProductsWithFilters method
|
||||
products = await repository.getProductsWithFilters(
|
||||
limitStart: 0,
|
||||
limitPageLength: pageSize,
|
||||
itemGroups: itemGroups,
|
||||
brands: brands,
|
||||
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
||||
searchKeyword: keyword,
|
||||
);
|
||||
|
||||
// If we got less than pageSize, there are no more products
|
||||
_hasMore = products.length >= pageSize;
|
||||
_currentPage = 1;
|
||||
return products;
|
||||
}
|
||||
@@ -125,12 +139,9 @@ class Products extends _$Products {
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMore) return;
|
||||
|
||||
// Watch dependencies to get current filters
|
||||
final selectedBrand = ref.read(selectedBrandProvider);
|
||||
// Read dependencies to get current filters (use read, not watch)
|
||||
final searchQuery = ref.read(searchQueryProvider);
|
||||
|
||||
// Don't paginate search results (already fetched all)
|
||||
if (searchQuery.isNotEmpty) return;
|
||||
final filters = ref.read(productFiltersProvider);
|
||||
|
||||
// Get repository
|
||||
final repository = await ref.read(productsRepositoryProvider.future);
|
||||
@@ -138,20 +149,46 @@ class Products extends _$Products {
|
||||
// Calculate pagination parameters
|
||||
final limitStart = _currentPage * pageSize;
|
||||
|
||||
// Fetch next page from API
|
||||
final getProductsUseCase = GetProducts(repository);
|
||||
var newProducts = await getProductsUseCase(
|
||||
// Build filter parameters (same logic as build() method)
|
||||
final List<String>? itemGroups = filters.productLines.isNotEmpty
|
||||
? filters.productLines.toList()
|
||||
: null;
|
||||
|
||||
// Use brands from productFiltersProvider (shared by chips and drawer)
|
||||
final List<String>? brands = filters.brands.isNotEmpty
|
||||
? filters.brands.toList()
|
||||
: null;
|
||||
|
||||
// Build item attributes from filter drawer (sizes, surfaces, colors)
|
||||
final List<Map<String, String>> itemAttributes = [];
|
||||
|
||||
// Add size attributes
|
||||
for (final size in filters.sizes) {
|
||||
itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size});
|
||||
}
|
||||
|
||||
// Add surface attributes
|
||||
for (final surface in filters.surfaces) {
|
||||
itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface});
|
||||
}
|
||||
|
||||
// Add color attributes
|
||||
for (final color in filters.colors) {
|
||||
itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color});
|
||||
}
|
||||
|
||||
final String? keyword = searchQuery.isNotEmpty ? searchQuery : null;
|
||||
|
||||
// Fetch next page using unified API
|
||||
final newProducts = await repository.getProductsWithFilters(
|
||||
limitStart: limitStart,
|
||||
limitPageLength: pageSize,
|
||||
itemGroups: itemGroups,
|
||||
brands: brands,
|
||||
itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null,
|
||||
searchKeyword: keyword,
|
||||
);
|
||||
|
||||
// Filter by brand if not 'all'
|
||||
if (selectedBrand != 'all') {
|
||||
newProducts = newProducts
|
||||
.where((product) => product.brand == selectedBrand)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// If we got less than pageSize, there are no more products
|
||||
_hasMore = newProducts.length >= pageSize;
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ final class ProductsProvider
|
||||
Products create() => Products();
|
||||
}
|
||||
|
||||
String _$productsHash() => r'5fe0fdb46c3a6845327221ff26ba5f3624fcf3bf';
|
||||
String _$productsHash() => r'6c55b22e75b912281feff3a68f84e488ccb7ab79';
|
||||
|
||||
/// Products Provider
|
||||
///
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/// Provider: Search Query Provider
|
||||
///
|
||||
/// Manages the current search query state for product filtering.
|
||||
/// Includes debounce functionality with 1-second delay and minimum 2 characters.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'search_query_provider.g.dart';
|
||||
@@ -12,28 +14,62 @@ part 'search_query_provider.g.dart';
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Features:
|
||||
/// - 1-second debounce delay
|
||||
/// - Only triggers search with 2+ non-whitespace characters
|
||||
/// - Auto-clears search when query is empty or too short
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// // Update the value (will debounce)
|
||||
/// ref.read(searchQueryProvider.notifier).updateQuery('gạch men');
|
||||
/// ```
|
||||
@riverpod
|
||||
class SearchQuery extends _$SearchQuery {
|
||||
Timer? _debounceTimer;
|
||||
|
||||
@override
|
||||
String build() {
|
||||
// Cancel timer when provider is disposed
|
||||
ref.onDispose(() {
|
||||
_debounceTimer?.cancel();
|
||||
});
|
||||
|
||||
return ''; // Default: no search filter
|
||||
}
|
||||
|
||||
/// Update search query
|
||||
/// Update search query with debounce
|
||||
///
|
||||
/// Only updates state after 1 second of no typing.
|
||||
/// Only triggers API call if query has 2+ non-whitespace characters.
|
||||
void updateQuery(String query) {
|
||||
state = query;
|
||||
// Cancel previous timer
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
// Trim whitespace from query
|
||||
final trimmedQuery = query.trim();
|
||||
|
||||
// If query is empty or too short, clear search immediately
|
||||
if (trimmedQuery.isEmpty || trimmedQuery.length < 2) {
|
||||
state = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up debounce timer (1 second)
|
||||
_debounceTimer = Timer(const Duration(seconds: 1), () {
|
||||
// Only update if query still meets requirements after delay
|
||||
if (trimmedQuery.length >= 2) {
|
||||
state = trimmedQuery;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Clear search query
|
||||
/// Clear search query immediately
|
||||
void clear() {
|
||||
_debounceTimer?.cancel();
|
||||
state = '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,18 @@ part of 'search_query_provider.dart';
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Features:
|
||||
/// - 1-second debounce delay
|
||||
/// - Only triggers search with 2+ non-whitespace characters
|
||||
/// - Auto-clears search when query is empty or too short
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// // Update the value (will debounce)
|
||||
/// ref.read(searchQueryProvider.notifier).updateQuery('gạch men');
|
||||
/// ```
|
||||
|
||||
@ProviderFor(SearchQuery)
|
||||
@@ -30,13 +35,18 @@ const searchQueryProvider = SearchQueryProvider._();
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Features:
|
||||
/// - 1-second debounce delay
|
||||
/// - Only triggers search with 2+ non-whitespace characters
|
||||
/// - Auto-clears search when query is empty or too short
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// // Update the value (will debounce)
|
||||
/// ref.read(searchQueryProvider.notifier).updateQuery('gạch men');
|
||||
/// ```
|
||||
final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
/// Search Query Provider
|
||||
@@ -44,13 +54,18 @@ final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Features:
|
||||
/// - 1-second debounce delay
|
||||
/// - Only triggers search with 2+ non-whitespace characters
|
||||
/// - Auto-clears search when query is empty or too short
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// // Update the value (will debounce)
|
||||
/// ref.read(searchQueryProvider.notifier).updateQuery('gạch men');
|
||||
/// ```
|
||||
const SearchQueryProvider._()
|
||||
: super(
|
||||
@@ -79,20 +94,25 @@ final class SearchQueryProvider extends $NotifierProvider<SearchQuery, String> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchQueryHash() => r'41ea2fa57593abc0cafe16598d8817584ba99ddc';
|
||||
String _$searchQueryHash() => r'3a4178c8c220a1016d20887d7bd97cd157f777f8';
|
||||
|
||||
/// Search Query Provider
|
||||
///
|
||||
/// Holds the current search query string for filtering products.
|
||||
/// Default is empty string which shows all products.
|
||||
///
|
||||
/// Features:
|
||||
/// - 1-second debounce delay
|
||||
/// - Only triggers search with 2+ non-whitespace characters
|
||||
/// - Auto-clears search when query is empty or too short
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // Read the current value
|
||||
/// final searchQuery = ref.watch(searchQueryProvider);
|
||||
///
|
||||
/// // Update the value
|
||||
/// ref.read(searchQueryProvider.notifier).state = 'gạch men';
|
||||
/// // Update the value (will debounce)
|
||||
/// ref.read(searchQueryProvider.notifier).updateQuery('gạch men');
|
||||
/// ```
|
||||
|
||||
abstract class _$SearchQuery extends $Notifier<String> {
|
||||
|
||||
@@ -8,18 +8,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/selected_brand_provider.dart';
|
||||
import 'package:worker/features/products/presentation/providers/product_filters_provider.dart';
|
||||
|
||||
/// Brand Filter Chips Widget
|
||||
///
|
||||
/// Displays brands as horizontally scrolling chips.
|
||||
/// Updates selected brand when tapped.
|
||||
/// Synced with filter drawer - both use productFiltersProvider.brands.
|
||||
/// Chips are single-select (tapping a brand clears others and sets just that one).
|
||||
class BrandFilterChips extends ConsumerWidget {
|
||||
const BrandFilterChips({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedBrand = ref.watch(selectedBrandProvider);
|
||||
final filtersState = ref.watch(productFiltersProvider);
|
||||
final filterOptionsAsync = ref.watch(productFilterOptionsProvider);
|
||||
|
||||
return filterOptionsAsync.when(
|
||||
@@ -40,7 +41,13 @@ class BrandFilterChips extends ConsumerWidget {
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
itemBuilder: (context, index) {
|
||||
final brand = allBrands[index];
|
||||
final isSelected = selectedBrand == brand.value;
|
||||
|
||||
// "Tất cả" is selected if no brands are selected
|
||||
// A brand chip is selected if it's the ONLY brand selected
|
||||
final isSelected = brand.value == 'all'
|
||||
? filtersState.brands.isEmpty
|
||||
: (filtersState.brands.length == 1 &&
|
||||
filtersState.brands.contains(brand.value));
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
@@ -54,9 +61,27 @@ class BrandFilterChips extends ConsumerWidget {
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
ref
|
||||
.read(selectedBrandProvider.notifier)
|
||||
.updateBrand(brand.value);
|
||||
final notifier = ref.read(productFiltersProvider.notifier);
|
||||
|
||||
if (brand.value == 'all') {
|
||||
// Clear all brand filters
|
||||
// Reset all brands by setting to empty set
|
||||
final currentBrands = List<String>.from(filtersState.brands);
|
||||
for (final b in currentBrands) {
|
||||
notifier.toggleBrand(b); // Toggle off each brand
|
||||
}
|
||||
} else {
|
||||
// Single-select: clear all other brands and set only this one
|
||||
final currentBrands = List<String>.from(filtersState.brands);
|
||||
|
||||
// First, clear all existing brands
|
||||
for (final b in currentBrands) {
|
||||
notifier.toggleBrand(b);
|
||||
}
|
||||
|
||||
// Then add the selected brand
|
||||
notifier.toggleBrand(brand.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor: AppColors.white,
|
||||
|
||||
@@ -15,9 +15,9 @@ import 'package:worker/features/products/presentation/providers/product_filter_o
|
||||
///
|
||||
/// A drawer that slides from the right with filter options:
|
||||
/// - Dòng sản phẩm (Product Line)
|
||||
/// - Không gian (Space)
|
||||
/// - Kích thước (Size)
|
||||
/// - Bề mặt (Surface)
|
||||
/// - Màu sắc (Color)
|
||||
/// - Thương hiệu (Brand)
|
||||
class ProductFilterDrawer extends ConsumerWidget {
|
||||
const ProductFilterDrawer({super.key});
|
||||
@@ -94,15 +94,23 @@ class ProductFilterDrawer extends ConsumerWidget {
|
||||
|
||||
// Attribute Groups (Colour, Size, Surface) - from API
|
||||
...filterOptions.attributeGroups.map((attrGroup) {
|
||||
// Dynamically map attribute to correct filter state
|
||||
final selectedValues = _getSelectedValuesForAttribute(
|
||||
attrGroup.attributeName,
|
||||
filtersState,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildAttributeGroup(
|
||||
title: attrGroup.attributeName,
|
||||
attributeGroup: attrGroup,
|
||||
selectedValues: filtersState.sizes, // TODO: Map to correct filter state
|
||||
onToggle: (value) => ref
|
||||
.read(productFiltersProvider.notifier)
|
||||
.toggleSize(value),
|
||||
selectedValues: selectedValues,
|
||||
onToggle: (value) => _toggleAttributeValue(
|
||||
ref,
|
||||
attrGroup.attributeName,
|
||||
value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
@@ -341,9 +349,9 @@ class ProductFilterDrawer extends ConsumerWidget {
|
||||
// ),
|
||||
// )
|
||||
// : null,
|
||||
value: selectedValues.contains(value.name),
|
||||
value: selectedValues.contains(value.attributeValue),
|
||||
onChanged: (bool? checked) {
|
||||
onToggle(value.name);
|
||||
onToggle(value.attributeValue);
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -354,4 +362,46 @@ class ProductFilterDrawer extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get selected values for a specific attribute based on its name
|
||||
Set<String> _getSelectedValuesForAttribute(
|
||||
String attributeName,
|
||||
ProductFiltersState filtersState,
|
||||
) {
|
||||
switch (attributeName) {
|
||||
case 'Kích thước':
|
||||
return filtersState.sizes;
|
||||
case 'Bề mặt':
|
||||
return filtersState.surfaces;
|
||||
case 'Màu sắc':
|
||||
return filtersState.colors;
|
||||
default:
|
||||
// For unknown attributes, return empty set
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle attribute value based on attribute name
|
||||
void _toggleAttributeValue(
|
||||
WidgetRef ref,
|
||||
String attributeName,
|
||||
String value,
|
||||
) {
|
||||
final notifier = ref.read(productFiltersProvider.notifier);
|
||||
|
||||
switch (attributeName) {
|
||||
case 'Kích thước':
|
||||
notifier.toggleSize(value);
|
||||
break;
|
||||
case 'Bề mặt':
|
||||
notifier.toggleSurface(value);
|
||||
break;
|
||||
case 'Màu sắc':
|
||||
notifier.toggleColor(value);
|
||||
break;
|
||||
default:
|
||||
// For unknown attributes, do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,20 @@ class ProductSearchBar extends ConsumerStatefulWidget {
|
||||
class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
late final TextEditingController _controller;
|
||||
late final FocusNode _focusNode;
|
||||
bool _hasText = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_focusNode = FocusNode();
|
||||
|
||||
// Listen to text changes to update clear button visibility
|
||||
_controller.addListener(() {
|
||||
setState(() {
|
||||
_hasText = _controller.text.isNotEmpty;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -53,7 +61,7 @@ class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: InputFieldSpecs.height,
|
||||
@@ -72,7 +80,7 @@ class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
|
||||
color: AppColors.grey500,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: _controller.text.isNotEmpty
|
||||
suffixIcon: _hasText
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.xmark,
|
||||
|
||||
Reference in New Issue
Block a user