diff --git a/lib/data/datasources/product_remote_datasource.dart b/lib/data/datasources/product_remote_datasource.dart index 5ce4e09..708c1b2 100644 --- a/lib/data/datasources/product_remote_datasource.dart +++ b/lib/data/datasources/product_remote_datasource.dart @@ -13,17 +13,27 @@ import 'auth_local_datasource.dart'; class ProductRemoteDatasource { final Dio dio = DioClient.instance; - Future> getProducts() async { + Future> getProducts({ + int page = 1, + int limit = Variables.defaultLimit, + String? categoryId, + }) async { try { final authData = await AuthLocalDataSource().getAuthData(); final url = '${Variables.baseUrl}/api/v1/products'; + Map queryParameters = { + 'page': page, + 'limit': limit, + }; + + if (categoryId != null) { + queryParameters['category_id'] = categoryId; + } + final response = await dio.get( url, - queryParameters: { - 'page': 1, - 'limit': 30, - }, + queryParameters: queryParameters, options: Options( headers: { 'Authorization': 'Bearer ${authData.token}', diff --git a/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart b/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart index 60daaf4..bff3d39 100644 --- a/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart +++ b/lib/presentation/home/bloc/product_loader/product_loader_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:enaklo_pos/data/datasources/product_remote_datasource.dart'; import 'package:enaklo_pos/data/models/response/product_response_model.dart'; @@ -9,15 +11,136 @@ part 'product_loader_bloc.freezed.dart'; class ProductLoaderBloc extends Bloc { final ProductRemoteDatasource _productRemoteDatasource; + + // Debouncing untuk mencegah multiple load more calls + Timer? _loadMoreDebounce; + bool _isLoadingMore = false; + ProductLoaderBloc(this._productRemoteDatasource) : super(ProductLoaderState.initial()) { - on<_GetProduct>((event, emit) async { - emit(const _Loading()); - final result = await _productRemoteDatasource.getProducts(); - result.fold( - (l) => emit(_Error(l)), - (r) => emit(_Loaded(r.data?.products ?? [])), + on<_GetProduct>(_onGetProduct); + on<_LoadMore>(_onLoadMore); + on<_Refresh>(_onRefresh); + } + + @override + Future close() { + _loadMoreDebounce?.cancel(); + return super.close(); + } + + // Debounce transformer untuk load more + // EventTransformer _debounceTransformer() { + // return (events, mapper) { + // return events + // .debounceTime(const Duration(milliseconds: 300)) + // .asyncExpand(mapper); + // }; + // } + + // Initial load + Future _onGetProduct( + _GetProduct event, + Emitter emit, + ) async { + emit(const _Loading()); + _isLoadingMore = false; // Reset loading state + + final result = await _productRemoteDatasource.getProducts( + page: 1, + limit: 10, + ); + + await result.fold( + (failure) async => emit(_Error(failure)), + (response) async { + final products = response.data?.products ?? []; + final hasReachedMax = products.length < 10; + + emit(_Loaded( + products: products, + hasReachedMax: hasReachedMax, + currentPage: 1, + isLoadingMore: false, + )); + }, + ); + } + + // Load more with enhanced debouncing + Future _onLoadMore( + _LoadMore event, + Emitter emit, + ) async { + final currentState = state; + + // Enhanced validation + if (currentState is! _Loaded || + currentState.hasReachedMax || + _isLoadingMore || + currentState.isLoadingMore) { + return; + } + + _isLoadingMore = true; + + // Emit loading more state + emit(currentState.copyWith(isLoadingMore: true)); + + final nextPage = currentState.currentPage + 1; + + try { + final result = await _productRemoteDatasource.getProducts( + page: nextPage, + limit: 10, ); - }); + + await result.fold( + (failure) async { + // On error, revert loading state but don't show error + // Just silently fail and allow retry + emit(currentState.copyWith(isLoadingMore: false)); + _isLoadingMore = false; + }, + (response) async { + final newProducts = response.data?.products ?? []; + + // Prevent duplicate products + final currentProductIds = + currentState.products.map((p) => p.id).toSet(); + final filteredNewProducts = newProducts + .where((product) => !currentProductIds.contains(product.id)) + .toList(); + + final allProducts = List.from(currentState.products) + ..addAll(filteredNewProducts); + + final hasReachedMax = newProducts.length < 10; + + emit(_Loaded( + products: allProducts, + hasReachedMax: hasReachedMax, + currentPage: nextPage, + isLoadingMore: false, + )); + + _isLoadingMore = false; + }, + ); + } catch (e) { + // Handle unexpected errors + emit(currentState.copyWith(isLoadingMore: false)); + _isLoadingMore = false; + } + } + + // Refresh data + Future _onRefresh( + _Refresh event, + Emitter emit, + ) async { + _isLoadingMore = false; + _loadMoreDebounce?.cancel(); + add(const _GetProduct()); } } diff --git a/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart b/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart index 9c2f0d8..bc82cbe 100644 --- a/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart +++ b/lib/presentation/home/bloc/product_loader/product_loader_bloc.freezed.dart @@ -19,32 +19,44 @@ mixin _$ProductLoaderEvent { @optionalTypeArgs TResult when({ required TResult Function() getProduct, + required TResult Function() loadMore, + required TResult Function() refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? getProduct, + TResult? Function()? loadMore, + TResult? Function()? refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeWhen({ TResult Function()? getProduct, + TResult Function()? loadMore, + TResult Function()? refresh, required TResult orElse(), }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult map({ required TResult Function(_GetProduct value) getProduct, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_GetProduct value)? getProduct, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, }) => throw _privateConstructorUsedError; @optionalTypeArgs TResult maybeMap({ TResult Function(_GetProduct value)? getProduct, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, required TResult orElse(), }) => throw _privateConstructorUsedError; @@ -113,6 +125,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult when({ required TResult Function() getProduct, + required TResult Function() loadMore, + required TResult Function() refresh, }) { return getProduct(); } @@ -121,6 +135,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult? whenOrNull({ TResult? Function()? getProduct, + TResult? Function()? loadMore, + TResult? Function()? refresh, }) { return getProduct?.call(); } @@ -129,6 +145,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult maybeWhen({ TResult Function()? getProduct, + TResult Function()? loadMore, + TResult Function()? refresh, required TResult orElse(), }) { if (getProduct != null) { @@ -141,6 +159,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult map({ required TResult Function(_GetProduct value) getProduct, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, }) { return getProduct(this); } @@ -149,6 +169,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult? mapOrNull({ TResult? Function(_GetProduct value)? getProduct, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, }) { return getProduct?.call(this); } @@ -157,6 +179,8 @@ class _$GetProductImpl implements _GetProduct { @optionalTypeArgs TResult maybeMap({ TResult Function(_GetProduct value)? getProduct, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, required TResult orElse(), }) { if (getProduct != null) { @@ -170,13 +194,237 @@ abstract class _GetProduct implements ProductLoaderEvent { const factory _GetProduct() = _$GetProductImpl; } +/// @nodoc +abstract class _$$LoadMoreImplCopyWith<$Res> { + factory _$$LoadMoreImplCopyWith( + _$LoadMoreImpl value, $Res Function(_$LoadMoreImpl) then) = + __$$LoadMoreImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadMoreImplCopyWithImpl<$Res> + extends _$ProductLoaderEventCopyWithImpl<$Res, _$LoadMoreImpl> + implements _$$LoadMoreImplCopyWith<$Res> { + __$$LoadMoreImplCopyWithImpl( + _$LoadMoreImpl _value, $Res Function(_$LoadMoreImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$LoadMoreImpl implements _LoadMore { + const _$LoadMoreImpl(); + + @override + String toString() { + return 'ProductLoaderEvent.loadMore()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadMoreImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getProduct, + required TResult Function() loadMore, + required TResult Function() refresh, + }) { + return loadMore(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getProduct, + TResult? Function()? loadMore, + TResult? Function()? refresh, + }) { + return loadMore?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getProduct, + TResult Function()? loadMore, + TResult Function()? refresh, + required TResult orElse(), + }) { + if (loadMore != null) { + return loadMore(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetProduct value) getProduct, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, + }) { + return loadMore(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetProduct value)? getProduct, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, + }) { + return loadMore?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetProduct value)? getProduct, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, + required TResult orElse(), + }) { + if (loadMore != null) { + return loadMore(this); + } + return orElse(); + } +} + +abstract class _LoadMore implements ProductLoaderEvent { + const factory _LoadMore() = _$LoadMoreImpl; +} + +/// @nodoc +abstract class _$$RefreshImplCopyWith<$Res> { + factory _$$RefreshImplCopyWith( + _$RefreshImpl value, $Res Function(_$RefreshImpl) then) = + __$$RefreshImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RefreshImplCopyWithImpl<$Res> + extends _$ProductLoaderEventCopyWithImpl<$Res, _$RefreshImpl> + implements _$$RefreshImplCopyWith<$Res> { + __$$RefreshImplCopyWithImpl( + _$RefreshImpl _value, $Res Function(_$RefreshImpl) _then) + : super(_value, _then); + + /// Create a copy of ProductLoaderEvent + /// with the given fields replaced by the non-null parameter values. +} + +/// @nodoc + +class _$RefreshImpl implements _Refresh { + const _$RefreshImpl(); + + @override + String toString() { + return 'ProductLoaderEvent.refresh()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$RefreshImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getProduct, + required TResult Function() loadMore, + required TResult Function() refresh, + }) { + return refresh(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getProduct, + TResult? Function()? loadMore, + TResult? Function()? refresh, + }) { + return refresh?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getProduct, + TResult Function()? loadMore, + TResult Function()? refresh, + required TResult orElse(), + }) { + if (refresh != null) { + return refresh(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetProduct value) getProduct, + required TResult Function(_LoadMore value) loadMore, + required TResult Function(_Refresh value) refresh, + }) { + return refresh(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetProduct value)? getProduct, + TResult? Function(_LoadMore value)? loadMore, + TResult? Function(_Refresh value)? refresh, + }) { + return refresh?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetProduct value)? getProduct, + TResult Function(_LoadMore value)? loadMore, + TResult Function(_Refresh value)? refresh, + required TResult orElse(), + }) { + if (refresh != null) { + return refresh(this); + } + return orElse(); + } +} + +abstract class _Refresh implements ProductLoaderEvent { + const factory _Refresh() = _$RefreshImpl; +} + /// @nodoc mixin _$ProductLoaderState { @optionalTypeArgs TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) loaded, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + loaded, required TResult Function(String message) error, }) => throw _privateConstructorUsedError; @@ -184,7 +432,9 @@ mixin _$ProductLoaderState { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? loaded, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult? Function(String message)? error, }) => throw _privateConstructorUsedError; @@ -192,7 +442,9 @@ mixin _$ProductLoaderState { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? loaded, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult Function(String message)? error, required TResult orElse(), }) => @@ -288,7 +540,9 @@ class _$InitialImpl implements _Initial { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) loaded, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + loaded, required TResult Function(String message) error, }) { return initial(); @@ -299,7 +553,9 @@ class _$InitialImpl implements _Initial { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? loaded, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult? Function(String message)? error, }) { return initial?.call(); @@ -310,7 +566,9 @@ class _$InitialImpl implements _Initial { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? loaded, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult Function(String message)? error, required TResult orElse(), }) { @@ -405,7 +663,9 @@ class _$LoadingImpl implements _Loading { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) loaded, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + loaded, required TResult Function(String message) error, }) { return loading(); @@ -416,7 +676,9 @@ class _$LoadingImpl implements _Loading { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? loaded, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult? Function(String message)? error, }) { return loading?.call(); @@ -427,7 +689,9 @@ class _$LoadingImpl implements _Loading { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? loaded, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult Function(String message)? error, required TResult orElse(), }) { @@ -485,7 +749,11 @@ abstract class _$$LoadedImplCopyWith<$Res> { _$LoadedImpl value, $Res Function(_$LoadedImpl) then) = __$$LoadedImplCopyWithImpl<$Res>; @useResult - $Res call({List products}); + $Res call( + {List products, + bool hasReachedMax, + int currentPage, + bool isLoadingMore}); } /// @nodoc @@ -502,12 +770,27 @@ class __$$LoadedImplCopyWithImpl<$Res> @override $Res call({ Object? products = null, + Object? hasReachedMax = null, + Object? currentPage = null, + Object? isLoadingMore = null, }) { return _then(_$LoadedImpl( - null == products + products: null == products ? _value._products : products // ignore: cast_nullable_to_non_nullable as List, + hasReachedMax: null == hasReachedMax + ? _value.hasReachedMax + : hasReachedMax // ignore: cast_nullable_to_non_nullable + as bool, + currentPage: null == currentPage + ? _value.currentPage + : currentPage // ignore: cast_nullable_to_non_nullable + as int, + isLoadingMore: null == isLoadingMore + ? _value.isLoadingMore + : isLoadingMore // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -515,7 +798,12 @@ class __$$LoadedImplCopyWithImpl<$Res> /// @nodoc class _$LoadedImpl implements _Loaded { - const _$LoadedImpl(final List products) : _products = products; + const _$LoadedImpl( + {required final List products, + required this.hasReachedMax, + required this.currentPage, + required this.isLoadingMore}) + : _products = products; final List _products; @override @@ -525,9 +813,16 @@ class _$LoadedImpl implements _Loaded { return EqualUnmodifiableListView(_products); } + @override + final bool hasReachedMax; + @override + final int currentPage; + @override + final bool isLoadingMore; + @override String toString() { - return 'ProductLoaderState.loaded(products: $products)'; + return 'ProductLoaderState.loaded(products: $products, hasReachedMax: $hasReachedMax, currentPage: $currentPage, isLoadingMore: $isLoadingMore)'; } @override @@ -535,12 +830,22 @@ class _$LoadedImpl implements _Loaded { return identical(this, other) || (other.runtimeType == runtimeType && other is _$LoadedImpl && - const DeepCollectionEquality().equals(other._products, _products)); + const DeepCollectionEquality().equals(other._products, _products) && + (identical(other.hasReachedMax, hasReachedMax) || + other.hasReachedMax == hasReachedMax) && + (identical(other.currentPage, currentPage) || + other.currentPage == currentPage) && + (identical(other.isLoadingMore, isLoadingMore) || + other.isLoadingMore == isLoadingMore)); } @override - int get hashCode => - Object.hash(runtimeType, const DeepCollectionEquality().hash(_products)); + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_products), + hasReachedMax, + currentPage, + isLoadingMore); /// Create a copy of ProductLoaderState /// with the given fields replaced by the non-null parameter values. @@ -555,10 +860,12 @@ class _$LoadedImpl implements _Loaded { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) loaded, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + loaded, required TResult Function(String message) error, }) { - return loaded(products); + return loaded(products, hasReachedMax, currentPage, isLoadingMore); } @override @@ -566,10 +873,12 @@ class _$LoadedImpl implements _Loaded { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? loaded, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult? Function(String message)? error, }) { - return loaded?.call(products); + return loaded?.call(products, hasReachedMax, currentPage, isLoadingMore); } @override @@ -577,12 +886,14 @@ class _$LoadedImpl implements _Loaded { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? loaded, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult Function(String message)? error, required TResult orElse(), }) { if (loaded != null) { - return loaded(products); + return loaded(products, hasReachedMax, currentPage, isLoadingMore); } return orElse(); } @@ -626,9 +937,16 @@ class _$LoadedImpl implements _Loaded { } abstract class _Loaded implements ProductLoaderState { - const factory _Loaded(final List products) = _$LoadedImpl; + const factory _Loaded( + {required final List products, + required final bool hasReachedMax, + required final int currentPage, + required final bool isLoadingMore}) = _$LoadedImpl; List get products; + bool get hasReachedMax; + int get currentPage; + bool get isLoadingMore; /// Create a copy of ProductLoaderState /// with the given fields replaced by the non-null parameter values. @@ -707,7 +1025,9 @@ class _$ErrorImpl implements _Error { TResult when({ required TResult Function() initial, required TResult Function() loading, - required TResult Function(List products) loaded, + required TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore) + loaded, required TResult Function(String message) error, }) { return error(message); @@ -718,7 +1038,9 @@ class _$ErrorImpl implements _Error { TResult? whenOrNull({ TResult? Function()? initial, TResult? Function()? loading, - TResult? Function(List products)? loaded, + TResult? Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult? Function(String message)? error, }) { return error?.call(message); @@ -729,7 +1051,9 @@ class _$ErrorImpl implements _Error { TResult maybeWhen({ TResult Function()? initial, TResult Function()? loading, - TResult Function(List products)? loaded, + TResult Function(List products, bool hasReachedMax, + int currentPage, bool isLoadingMore)? + loaded, TResult Function(String message)? error, required TResult orElse(), }) { diff --git a/lib/presentation/home/bloc/product_loader/product_loader_event.dart b/lib/presentation/home/bloc/product_loader/product_loader_event.dart index 33f9ad3..9b3ac2c 100644 --- a/lib/presentation/home/bloc/product_loader/product_loader_event.dart +++ b/lib/presentation/home/bloc/product_loader/product_loader_event.dart @@ -3,4 +3,6 @@ part of 'product_loader_bloc.dart'; @freezed class ProductLoaderEvent with _$ProductLoaderEvent { const factory ProductLoaderEvent.getProduct() = _GetProduct; + const factory ProductLoaderEvent.loadMore() = _LoadMore; + const factory ProductLoaderEvent.refresh() = _Refresh; } diff --git a/lib/presentation/home/bloc/product_loader/product_loader_state.dart b/lib/presentation/home/bloc/product_loader/product_loader_state.dart index c2a3f2c..1464f78 100644 --- a/lib/presentation/home/bloc/product_loader/product_loader_state.dart +++ b/lib/presentation/home/bloc/product_loader/product_loader_state.dart @@ -4,6 +4,11 @@ part of 'product_loader_bloc.dart'; class ProductLoaderState with _$ProductLoaderState { const factory ProductLoaderState.initial() = _Initial; const factory ProductLoaderState.loading() = _Loading; - const factory ProductLoaderState.loaded(List products) = _Loaded; + const factory ProductLoaderState.loaded({ + required List products, + required bool hasReachedMax, + required int currentPage, + required bool isLoadingMore, + }) = _Loaded; const factory ProductLoaderState.error(String message) = _Error; } diff --git a/lib/presentation/home/pages/home_page.dart b/lib/presentation/home/pages/home_page.dart index e42a147..04e64fa 100644 --- a/lib/presentation/home/pages/home_page.dart +++ b/lib/presentation/home/pages/home_page.dart @@ -38,6 +38,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { final searchController = TextEditingController(); + final ScrollController scrollController = ScrollController(); String searchQuery = ''; test() async { @@ -140,171 +141,204 @@ class _HomePageState extends State { ), BlocBuilder( builder: (context, state) { - return Expanded( - child: CustomTabBarV2( - tabTitles: const [ - 'Semua', - 'Makanan', - 'Minuman', - 'Paket' - ], - tabViews: [ - // All Products Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - final filteredProducts = - _filterProducts(products); - if (filteredProducts.isEmpty) { + return NotificationListener( + onNotification: (notification) { + return state.maybeWhen( + orElse: () => false, + loaded: (products, hasReachedMax, currentPage, + isLoadingMore) { + if (notification is ScrollEndNotification && + scrollController.position.extentAfter == + 0) { + context.read().add( + const ProductLoaderEvent.loadMore()); + return true; + } + + return true; + }, + ); + }, + child: Expanded( + child: CustomTabBarV2( + tabTitles: const [ + 'Semua', + 'Makanan', + 'Minuman', + 'Paket' + ], + tabViews: [ + // All Products Tab + SizedBox( + child: state.maybeWhen(orElse: () { return const Center( - child: Text('No Items Found'), + child: CircularProgressIndicator(), ); - } - return GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 180, - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 180 / 240, - ), - itemBuilder: (context, index) => - ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ), - ); - }), - ), - // Makanan Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { + }, loading: () { return const Center( - child: Text('No Items'), + child: CircularProgressIndicator(), ); - } - final filteredProducts = - _filterProductsByCategory(products, 1); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) => - ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ), - ); - }), - ), - // Minuman Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { + }, loaded: (products, hashasReachedMax, + currentPage, isLoadingMore) { + final filteredProducts = + _filterProducts(products); + if (filteredProducts.isEmpty) { + return const Center( + child: Text('No Items Found'), + ); + } + return GridView.builder( + itemCount: filteredProducts.length, + controller: scrollController, + padding: const EdgeInsets.all(16), + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 180, + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 180 / 240, + ), + itemBuilder: (context, index) => + ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ), + ); + }), + ), + // Makanan Tab + SizedBox( + child: state.maybeWhen(orElse: () { return const Center( - child: Text('No Items'), + child: CircularProgressIndicator(), ); - } - final filteredProducts = - _filterProductsByCategory(products, 2); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) { - return ProductCard( + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products, hashasReachedMax, + currentPage, isLoadingMore) { + if (products.isEmpty) { + return const Center( + child: Text('No Items'), + ); + } + final filteredProducts = + _filterProductsByCategory( + products, 1); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: + filteredProducts.length, + padding: const EdgeInsets.all(16), + controller: scrollController, + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) => + ProductCard( data: filteredProducts[index], onCartButton: () {}, - ); - }, - ); - }), - ), - // Snack Tab - SizedBox( - child: state.maybeWhen(orElse: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); - }, loaded: (products) { - if (products.isEmpty) { + ), + ); + }), + ), + // Minuman Tab + SizedBox( + child: state.maybeWhen(orElse: () { return const Center( - child: Text('No Items'), + child: CircularProgressIndicator(), ); - } - final filteredProducts = - _filterProductsByCategory(products, 3); - return filteredProducts.isEmpty - ? const _IsEmpty() - : GridView.builder( - itemCount: filteredProducts.length, - padding: const EdgeInsets.all(16), - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - 200, // Lebar maksimal tiap item (bisa kamu ubah) - mainAxisSpacing: 30, - crossAxisSpacing: 30, - childAspectRatio: 0.85, - ), - itemBuilder: (context, index) { - return ProductCard( - data: filteredProducts[index], - onCartButton: () {}, - ); - }, - ); - }), - ), - ], + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products, hashasReachedMax, + currentPage, isLoadingMore) { + if (products.isEmpty) { + return const Center( + child: Text('No Items'), + ); + } + final filteredProducts = + _filterProductsByCategory( + products, 2); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: + filteredProducts.length, + padding: const EdgeInsets.all(16), + controller: scrollController, + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ); + }, + ); + }), + ), + // Snack Tab + SizedBox( + child: state.maybeWhen(orElse: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loading: () { + return const Center( + child: CircularProgressIndicator(), + ); + }, loaded: (products, hashasReachedMax, + currentPage, isLoadingMore) { + if (products.isEmpty) { + return const Center( + child: Text('No Items'), + ); + } + final filteredProducts = + _filterProductsByCategory( + products, 3); + return filteredProducts.isEmpty + ? const _IsEmpty() + : GridView.builder( + itemCount: + filteredProducts.length, + padding: const EdgeInsets.all(16), + controller: scrollController, + gridDelegate: + SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 200, // Lebar maksimal tiap item (bisa kamu ubah) + mainAxisSpacing: 30, + crossAxisSpacing: 30, + childAspectRatio: 0.85, + ), + itemBuilder: (context, index) { + return ProductCard( + data: filteredProducts[index], + onCartButton: () {}, + ); + }, + ); + }), + ), + ], + ), ), ); },