diff --git a/lib/core/network/api_interceptor.dart b/lib/core/network/api_interceptor.dart index d3f7a9d..acc8b4c 100644 --- a/lib/core/network/api_interceptor.dart +++ b/lib/core/network/api_interceptor.dart @@ -10,6 +10,7 @@ library; import 'dart:developer' as developer; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -569,10 +570,10 @@ Future authInterceptor(Ref ref, Dio dio) async { @riverpod LoggingInterceptor loggingInterceptor(Ref ref) { // Only enable logging in debug mode - const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter + const bool isDebug = kDebugMode; // TODO: Replace with kDebugMode from Flutter return LoggingInterceptor( - enableRequestLogging: false, + enableRequestLogging: true, enableResponseLogging: isDebug, enableErrorLogging: isDebug, ); diff --git a/lib/core/network/api_interceptor.g.dart b/lib/core/network/api_interceptor.g.dart index a21528d..0699841 100644 --- a/lib/core/network/api_interceptor.g.dart +++ b/lib/core/network/api_interceptor.g.dart @@ -189,7 +189,7 @@ final class LoggingInterceptorProvider } String _$loggingInterceptorHash() => - r'6afa480caa6fcd723dab769bb01601b8a37e20fd'; + r'79e90e0eb78663d2645d2d7c467e01bc18a30551'; /// Provider for ErrorTransformerInterceptor diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 626492b..cb528ff 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -13,6 +13,7 @@ import 'dart:developer' as developer; import 'package:dio/dio.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart'; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -248,13 +249,13 @@ class CustomCurlLoggerInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { final curl = _cURLRepresentation(options); - // debugPrint( - // '╔╣ CURL Request ╠══════════════════════════════════════════════════', - // ); - // debugPrint(curl); - // debugPrint( - // '╚═════════════════════════════════════════════════════════════════', - // ); + debugPrint( + '╔╣ CURL Request ╠══════════════════════════════════════════════════', + ); + debugPrint(curl); + debugPrint( + '╚═════════════════════════════════════════════════════════════════', + ); // Also log to dart:developer for better filtering in DevTools developer.log(curl, name: 'DIO_CURL', time: DateTime.now()); handler.next(options); @@ -467,7 +468,7 @@ Future dio(Ref ref) async { // Add interceptors in order // 1. Custom Curl interceptor (first to log cURL commands) // Uses debugPrint and developer.log for better visibility - ..interceptors.add(CustomCurlLoggerInterceptor()) + // ..interceptors.add(CustomCurlLoggerInterceptor()) // 2. Logging interceptor ..interceptors.add(ref.watch(loggingInterceptorProvider)) // 3. Auth interceptor (add tokens to requests) diff --git a/lib/core/network/dio_client.g.dart b/lib/core/network/dio_client.g.dart index 31d3f64..271c30e 100644 --- a/lib/core/network/dio_client.g.dart +++ b/lib/core/network/dio_client.g.dart @@ -131,7 +131,7 @@ final class DioProvider } } -String _$dioHash() => r'd15bfe824d6501e5cbd56ff152de978030d97be4'; +String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7'; /// Provider for DioClient diff --git a/lib/core/services/frappe_auth_service.dart b/lib/core/services/frappe_auth_service.dart index 1c53d38..246e9e1 100644 --- a/lib/core/services/frappe_auth_service.dart +++ b/lib/core/services/frappe_auth_service.dart @@ -87,7 +87,7 @@ class FrappeAuthService { } } - final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}'; + const url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}'; // Build cookie header final storedSession = await getStoredSession(); diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart index 33066a3..b5e8ef1 100644 --- a/lib/features/auth/presentation/providers/auth_provider.dart +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -6,7 +6,6 @@ /// Uses Riverpod 3.0 with code generation for type-safe state management. library; -import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:worker/core/constants/api_constants.dart'; @@ -14,7 +13,6 @@ import 'package:worker/core/network/dio_client.dart'; import 'package:worker/core/services/frappe_auth_service.dart'; import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart'; -import 'package:worker/features/auth/data/models/auth_session_model.dart'; import 'package:worker/features/auth/domain/entities/user.dart'; part 'auth_provider.g.dart'; @@ -80,10 +78,6 @@ class Auth extends _$Auth { Future get _frappeAuthService async => await ref.read(frappeAuthServiceProvider.future); - /// Get auth remote data source - Future get _remoteDataSource async => - await ref.read(authRemoteDataSourceProvider.future); - /// Initialize with saved session if available @override Future build() async { @@ -170,7 +164,6 @@ class Auth extends _$Auth { } final frappeService = await _frappeAuthService; - final remoteDataSource = await _remoteDataSource; // Get current session (should exist from app startup) final currentSession = await frappeService.getStoredSession(); @@ -183,22 +176,8 @@ class Auth extends _$Auth { } } - // Get stored session again - final session = await frappeService.getStoredSession(); - if (session == null) { - throw Exception('Session not available'); - } - - // Call login API with current session - final loginResponse = await remoteDataSource.login( - phone: phoneNumber, - csrfToken: session['csrfToken']!, - sid: session['sid']!, - password: password, // Reserved for future use - ); - - // Update FlutterSecureStorage with new authenticated session - await frappeService.login(phoneNumber, password: password); + // Call login API and store session + final loginResponse = await frappeService.login(phoneNumber, password: password); // Save rememberMe preference await _localDataSource.saveRememberMe(rememberMe); diff --git a/lib/features/auth/presentation/providers/auth_provider.g.dart b/lib/features/auth/presentation/providers/auth_provider.g.dart index 4bdfc92..2146fc8 100644 --- a/lib/features/auth/presentation/providers/auth_provider.g.dart +++ b/lib/features/auth/presentation/providers/auth_provider.g.dart @@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider { Auth create() => Auth(); } -String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae'; +String _$authHash() => r'd851980cad7a624f00eba69e19d8a4fee22008e7'; /// Authentication Provider /// diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 14a3dea..16dd24f 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/utils/extensions.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; @@ -133,10 +134,7 @@ class _HomePageState extends ConsumerState { }, ) : const SizedBox.shrink(), - loading: () => const Padding( - padding: EdgeInsets.all(16), - child: Center(child: CircularProgressIndicator()), - ), + loading: () => _buildPromotionsShimmer(colorScheme), error: (error, stack) => const SizedBox.shrink(), ), ), @@ -241,4 +239,93 @@ class _HomePageState extends ConsumerState { ), ); } + + /// Build shimmer loading for promotions section + Widget _buildPromotionsShimmer(ColorScheme colorScheme) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Title shimmer + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Chương trình ưu đãi', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: colorScheme.onSurface, + ), + ), + ), + const SizedBox(height: 12), + // Cards shimmer + SizedBox( + height: 210, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: 3, + itemBuilder: (context, index) { + return Shimmer.fromColors( + baseColor: colorScheme.surfaceContainerHighest, + highlightColor: colorScheme.surface, + child: Container( + width: 280, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image placeholder + Container( + height: 140, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + ), + ), + ), + // Text placeholders + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 200, + height: 16, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + width: 140, + height: 12, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } } diff --git a/lib/features/home/presentation/providers/promotions_provider.dart b/lib/features/home/presentation/providers/promotions_provider.dart index 1bb4b09..74d4a8f 100644 --- a/lib/features/home/presentation/providers/promotions_provider.dart +++ b/lib/features/home/presentation/providers/promotions_provider.dart @@ -1,23 +1,25 @@ /// Provider: Promotions Provider /// /// Manages the state of promotions data using Riverpod. -/// Provides access to active promotions throughout the app. +/// Uses the same data source as news articles (single API call). /// /// Uses AsyncNotifierProvider for automatic loading, error, and data states. library; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:worker/features/home/data/datasources/home_local_datasource.dart'; -import 'package:worker/features/home/data/repositories/home_repository_impl.dart'; import 'package:worker/features/home/domain/entities/promotion.dart'; -import 'package:worker/features/home/domain/usecases/get_promotions.dart'; +import 'package:worker/features/news/presentation/providers/news_provider.dart'; part 'promotions_provider.g.dart'; +/// Max number of promotions to display on home page +const int _maxPromotions = 5; + /// Promotions Provider /// -/// Fetches and caches the list of active promotions. -/// Automatically handles loading, error, and data states. +/// Uses the same data source as news articles to avoid duplicate API calls. +/// Converts NewsArticle to Promotion entity for display in PromotionSlider. +/// Limited to 5 items max. /// /// Usage: /// ```dart @@ -31,33 +33,22 @@ part 'promotions_provider.g.dart'; /// ); /// ``` @riverpod -class PromotionsNotifier extends _$PromotionsNotifier { - @override - Future> build() async { - // Initialize dependencies - final localDataSource = const HomeLocalDataSourceImpl(); - final repository = HomeRepositoryImpl(localDataSource: localDataSource); - final useCase = GetPromotions(repository); +Future> promotions(Ref ref) async { + // Use newsArticles provider (same API call, no duplicate request) + final articles = await ref.watch(newsArticlesProvider.future); - // Fetch promotions (only active ones) - return await useCase(); - } + // Take max 5 articles and convert to Promotion + final limitedArticles = articles.take(_maxPromotions).toList(); - /// Refresh promotions data - /// - /// Forces a refresh from the server (when API is available). - /// Updates the cached state with fresh data. - Future refresh() async { - // Set loading state - state = const AsyncValue.loading(); - - // Fetch fresh data - state = await AsyncValue.guard(() async { - final localDataSource = const HomeLocalDataSourceImpl(); - final repository = HomeRepositoryImpl(localDataSource: localDataSource); - final useCase = GetPromotions(repository); - - return await useCase.refresh(); - }); - } + return limitedArticles.map((article) { + final now = DateTime.now(); + return Promotion( + id: article.id, + title: article.title, + description: article.excerpt, + imageUrl: article.imageUrl, + startDate: article.publishedDate, + endDate: now.add(const Duration(days: 365)), // Always active + ); + }).toList(); } diff --git a/lib/features/home/presentation/providers/promotions_provider.g.dart b/lib/features/home/presentation/providers/promotions_provider.g.dart index 42f6587..ea8770c 100644 --- a/lib/features/home/presentation/providers/promotions_provider.g.dart +++ b/lib/features/home/presentation/providers/promotions_provider.g.dart @@ -10,8 +10,9 @@ part of 'promotions_provider.dart'; // ignore_for_file: type=lint, type=warning /// Promotions Provider /// -/// Fetches and caches the list of active promotions. -/// Automatically handles loading, error, and data states. +/// Uses the same data source as news articles to avoid duplicate API calls. +/// Converts NewsArticle to Promotion entity for display in PromotionSlider. +/// Limited to 5 items max. /// /// Usage: /// ```dart @@ -25,13 +26,14 @@ part of 'promotions_provider.dart'; /// ); /// ``` -@ProviderFor(PromotionsNotifier) -const promotionsProvider = PromotionsNotifierProvider._(); +@ProviderFor(promotions) +const promotionsProvider = PromotionsProvider._(); /// Promotions Provider /// -/// Fetches and caches the list of active promotions. -/// Automatically handles loading, error, and data states. +/// Uses the same data source as news articles to avoid duplicate API calls. +/// Converts NewsArticle to Promotion entity for display in PromotionSlider. +/// Limited to 5 items max. /// /// Usage: /// ```dart @@ -44,12 +46,20 @@ const promotionsProvider = PromotionsNotifierProvider._(); /// error: (error, stack) => ErrorWidget(error), /// ); /// ``` -final class PromotionsNotifierProvider - extends $AsyncNotifierProvider> { + +final class PromotionsProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with $FutureModifier>, $FutureProvider> { /// Promotions Provider /// - /// Fetches and caches the list of active promotions. - /// Automatically handles loading, error, and data states. + /// Uses the same data source as news articles to avoid duplicate API calls. + /// Converts NewsArticle to Promotion entity for display in PromotionSlider. + /// Limited to 5 items max. /// /// Usage: /// ```dart @@ -62,7 +72,7 @@ final class PromotionsNotifierProvider /// error: (error, stack) => ErrorWidget(error), /// ); /// ``` - const PromotionsNotifierProvider._() + const PromotionsProvider._() : super( from: null, argument: null, @@ -74,48 +84,18 @@ final class PromotionsNotifierProvider ); @override - String debugGetCreateSourceHash() => _$promotionsNotifierHash(); + String debugGetCreateSourceHash() => _$promotionsHash(); @$internal @override - PromotionsNotifier create() => PromotionsNotifier(); -} + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); -String _$promotionsNotifierHash() => - r'3cd866c74ba11c6519e9b63521e1757ef117c7a9'; - -/// Promotions Provider -/// -/// Fetches and caches the list of active promotions. -/// Automatically handles loading, error, and data states. -/// -/// Usage: -/// ```dart -/// // In a ConsumerWidget -/// final promotionsAsync = ref.watch(promotionsProvider); -/// -/// promotionsAsync.when( -/// data: (promotions) => PromotionSlider(promotions: promotions), -/// loading: () => CircularProgressIndicator(), -/// error: (error, stack) => ErrorWidget(error), -/// ); -/// ``` - -abstract class _$PromotionsNotifier extends $AsyncNotifier> { - FutureOr> build(); - @$mustCallSuper @override - void runBuild() { - final created = build(); - final ref = this.ref as $Ref>, List>; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier>, List>, - AsyncValue>, - Object?, - Object? - >; - element.handleValue(ref, created); + FutureOr> create(Ref ref) { + return promotions(ref); } } + +String _$promotionsHash() => r'2eac0298d2b84ad5cc50faa6b8a015dbf7b7a1d3'; diff --git a/lib/features/products/presentation/pages/products_page.dart b/lib/features/products/presentation/pages/products_page.dart index 5afc747..7cf0b95 100644 --- a/lib/features/products/presentation/pages/products_page.dart +++ b/lib/features/products/presentation/pages/products_page.dart @@ -11,7 +11,6 @@ import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; -import 'package:worker/features/products/presentation/providers/product_filter_options_provider.dart'; import 'package:worker/features/products/presentation/providers/products_provider.dart'; import 'package:worker/features/products/presentation/widgets/brand_filter_chips.dart'; import 'package:worker/features/products/presentation/widgets/product_filter_drawer.dart'; @@ -36,8 +35,7 @@ class ProductsPage extends ConsumerWidget { final l10n = AppLocalizations.of(context); final productsAsync = ref.watch(productsProvider); - // Preload filter options for better UX when opening filter drawer - ref.watch(productFilterOptionsProvider); + // Filter options loaded lazily when filter drawer is opened (not here) return Scaffold( backgroundColor: colorScheme.surfaceContainerLowest, @@ -105,8 +103,10 @@ class ProductsPage extends ConsumerWidget { ), ), - // Brand Filter Chips - const BrandFilterChips(), + // Brand Filter Chips - only show after products are loaded + productsAsync.hasValue + ? const BrandFilterChips() + : const SizedBox(height: 48.0), // Products Grid Expanded( diff --git a/lib/features/products/presentation/providers/products_provider.dart b/lib/features/products/presentation/providers/products_provider.dart index 2b761fa..8869d9d 100644 --- a/lib/features/products/presentation/providers/products_provider.dart +++ b/lib/features/products/presentation/providers/products_provider.dart @@ -86,49 +86,56 @@ class Products extends _$Products { // Get repository with injected data sources final repository = await ref.watch(productsRepositoryProvider.future); - // Fetch first page of products using unified API + // Fetch first page of products List products; - // Build filter parameters from filter drawer - final List? itemGroups = filters.productLines.isNotEmpty - ? filters.productLines.toList() - : null; + // Check if any filters or search are active + final hasFilters = filters.hasActiveFilters; + final hasSearch = searchQuery.isNotEmpty; - // Use brands from productFiltersProvider (shared by chips and drawer) - final List? brands = filters.brands.isNotEmpty - ? filters.brands.toList() - : null; + if (!hasFilters && !hasSearch) { + // No filters/search: Use simple getAllProducts for faster initial load + products = await repository.getAllProducts( + limitStart: 0, + limitPageLength: pageSize, + ); + } else { + // Filters/search active: Use getProductsWithFilters + final List? itemGroups = filters.productLines.isNotEmpty + ? filters.productLines.toList() + : null; - // Build item attributes from filter drawer (sizes, surfaces, colors) - final List> itemAttributes = []; + final List? brands = filters.brands.isNotEmpty + ? filters.brands.toList() + : null; - // Add size attributes - for (final size in filters.sizes) { - itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); + // Build item attributes from filter drawer (sizes, surfaces, colors) + final List> itemAttributes = []; + + for (final size in filters.sizes) { + itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); + } + + for (final surface in filters.surfaces) { + itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface}); + } + + for (final color in filters.colors) { + itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color}); + } + + final String? keyword = hasSearch ? searchQuery : null; + + products = await repository.getProductsWithFilters( + limitStart: 0, + limitPageLength: pageSize, + itemGroups: itemGroups, + brands: brands, + itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null, + searchKeyword: keyword, + ); } - // 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; @@ -149,46 +156,54 @@ class Products extends _$Products { // Calculate pagination parameters final limitStart = _currentPage * pageSize; - // Build filter parameters (same logic as build() method) - final List? itemGroups = filters.productLines.isNotEmpty - ? filters.productLines.toList() - : null; + // Check if any filters or search are active + final hasFilters = filters.hasActiveFilters; + final hasSearch = searchQuery.isNotEmpty; - // Use brands from productFiltersProvider (shared by chips and drawer) - final List? brands = filters.brands.isNotEmpty - ? filters.brands.toList() - : null; + List newProducts; - // Build item attributes from filter drawer (sizes, surfaces, colors) - final List> itemAttributes = []; + if (!hasFilters && !hasSearch) { + // No filters/search: Use simple getAllProducts + newProducts = await repository.getAllProducts( + limitStart: limitStart, + limitPageLength: pageSize, + ); + } else { + // Filters/search active: Use getProductsWithFilters + final List? itemGroups = filters.productLines.isNotEmpty + ? filters.productLines.toList() + : null; - // Add size attributes - for (final size in filters.sizes) { - itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); + final List? brands = filters.brands.isNotEmpty + ? filters.brands.toList() + : null; + + final List> itemAttributes = []; + + for (final size in filters.sizes) { + itemAttributes.add({'attribute': 'Kích thước', 'attribute_value': size}); + } + + for (final surface in filters.surfaces) { + itemAttributes.add({'attribute': 'Bề mặt', 'attribute_value': surface}); + } + + for (final color in filters.colors) { + itemAttributes.add({'attribute': 'Màu sắc', 'attribute_value': color}); + } + + final String? keyword = hasSearch ? searchQuery : null; + + newProducts = await repository.getProductsWithFilters( + limitStart: limitStart, + limitPageLength: pageSize, + itemGroups: itemGroups, + brands: brands, + itemAttributes: itemAttributes.isNotEmpty ? itemAttributes : null, + searchKeyword: keyword, + ); } - // 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, - ); - // If we got less than pageSize, there are no more products _hasMore = newProducts.length >= pageSize; diff --git a/lib/features/products/presentation/providers/products_provider.g.dart b/lib/features/products/presentation/providers/products_provider.g.dart index f6b7ed9..dcce576 100644 --- a/lib/features/products/presentation/providers/products_provider.g.dart +++ b/lib/features/products/presentation/providers/products_provider.g.dart @@ -228,7 +228,7 @@ final class ProductsProvider Products create() => Products(); } -String _$productsHash() => r'6c55b22e75b912281feff3a68f84e488ccb7ab79'; +String _$productsHash() => r'a4f416712cdbf2e633622c65b1fdc95686e31fa4'; /// Products Provider ///