import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:line_icons/line_icons.dart'; import 'package:shimmer/shimmer.dart'; import '../../../application/category/category_loader/category_loader_bloc.dart'; import '../../../application/product/product_loader/product_loader_bloc.dart'; import '../../../common/theme/theme.dart'; import '../../../domain/category/category.dart'; import '../../../domain/product/product.dart'; import '../../../injection.dart'; import '../../components/appbar/appbar.dart'; import '../../components/button/button.dart'; import '../../components/widgets/empty_widget.dart'; import 'widgets/category_delegate.dart'; import 'widgets/product_card.dart'; import 'widgets/product_tile.dart'; @RoutePage() class ProductPage extends StatefulWidget implements AutoRouteWrapper { const ProductPage({super.key}); @override State createState() => _ProductPageState(); @override Widget wrappedRoute(BuildContext context) => MultiBlocProvider( providers: [ BlocProvider( create: (context) => getIt()..add(CategoryLoaderEvent.fetched()), ), BlocProvider( create: (context) => getIt() ..add(ProductLoaderEvent.fetched(isRefresh: true)), ), ], child: this, ); } enum ViewType { grid, list } class _ProductPageState extends State with TickerProviderStateMixin { Category selectedCategory = Category.addAllData(); ViewType currentViewType = ViewType.grid; ScrollController scrollController = ScrollController(); @override initState() { super.initState(); } @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => previous.categoryId != current.categoryId, listener: (context, state) { context.read().add( ProductLoaderEvent.fetched(isRefresh: true), ); }, child: BlocBuilder( builder: (context, state) { return Scaffold( backgroundColor: AppColor.background, body: NotificationListener( onNotification: (notification) { if (notification is ScrollEndNotification && scrollController.position.extentAfter == 0) { context.read().add( ProductLoaderEvent.fetched(), ); return true; } return true; }, child: CustomScrollView( controller: scrollController, slivers: [ _buildSliverAppBar(), _buildCategoryFilter(), _buildProductContent(state), ], ), ), ); }, ), ); } Widget _buildSliverAppBar() { return SliverAppBar( expandedHeight: 120.0, floating: false, pinned: true, elevation: 0, flexibleSpace: CustomAppBar(title: 'Produk'), actions: [ ActionIconButton(onTap: () {}, icon: LineIcons.search), ActionIconButton( onTap: _toggleViewType, icon: currentViewType == ViewType.grid ? LineIcons.list : LineIcons.thLarge, ), ], ); } Widget _buildCategoryFilter() { return BlocBuilder( builder: (context, state) { if (state.isFetching && state.categories.isEmpty) { return _buildCategoryShimmer(); } return SliverPersistentHeader( pinned: true, delegate: ProductCategoryHeaderDelegate( categories: state.categories, selectedCategory: selectedCategory, onCategoryChanged: (category) { setState(() { selectedCategory = category; }); if (category.id == Category.addAllData().id) { context.read().add( ProductLoaderEvent.categoryIdChanged(''), ); } else { context.read().add( ProductLoaderEvent.categoryIdChanged(category.id), ); } }, ), ); }, ); } Widget _buildCategoryShimmer() { return SliverToBoxAdapter( child: Container( height: 60, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Row( children: List.generate( 4, (index) => Container( margin: EdgeInsets.only(right: index < 3 ? 12 : 0), width: 80, height: 35, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), ), ), ), ), ), ); } Widget _buildProductContent(ProductLoaderState state) { if (state.isFetching && state.products.isEmpty) { return currentViewType == ViewType.grid ? _buildProductGridShimmer() : _buildProductListShimmer(); } if (state.products.isEmpty && !state.isFetching) { return SliverToBoxAdapter( child: EmptyWidget( title: 'Tidak ada produk ditemukan', message: 'Coba ubah filter atau tambah produk baru', ), ); } return currentViewType == ViewType.grid ? _buildProductGrid(state.products) : _buildProductList(state.products); } Widget _buildProductGridShimmer() { return SliverPadding( padding: const EdgeInsets.all(16.0), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.85, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, ), delegate: SliverChildBuilderDelegate((context, index) { return _buildProductTileShimmer(); }, childCount: 6), // Show 6 shimmer items ), ); } Widget _buildProductTileShimmer() { return Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image shimmer Expanded( flex: 3, child: Container( width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), ), ), // Content shimmer Expanded( flex: 2, child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, height: 16, color: Colors.white, ), const SizedBox(height: 8), Container(width: 100, height: 12, color: Colors.white), const Spacer(), Container(width: 80, height: 14, color: Colors.white), ], ), ), ), ], ), ), ); } Widget _buildProductListShimmer() { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { return _buildProductListItemShimmer(); }, childCount: 8), // Show 8 shimmer items ), ); } Widget _buildProductListItemShimmer() { return Container( margin: const EdgeInsets.only(bottom: 12.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 6, offset: const Offset(0, 2), ), ], ), child: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ // Image shimmer Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), ), const SizedBox(width: 12), // Content shimmer Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: double.infinity, height: 16, color: Colors.white, ), const SizedBox(height: 8), Container(width: 120, height: 12, color: Colors.white), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container(width: 100, height: 16, color: Colors.white), Container( width: 60, height: 24, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), ), ], ), ], ), ), const SizedBox(width: 8), // Action button shimmer Container(width: 24, height: 24, color: Colors.white), ], ), ), ), ); } Widget _buildProductGrid(List products) { return SliverPadding( padding: const EdgeInsets.all(16.0), sliver: SliverGrid( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 0.75, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, ), delegate: SliverChildBuilderDelegate((context, index) { final product = products[index]; return ProductTile(product: product, onTap: () {}); }, childCount: products.length), ), ); } Widget _buildProductList(List products) { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { final product = products[index]; return ProductCard(product: product); }, childCount: products.length), ), ); } void _toggleViewType() { setState(() { currentViewType = currentViewType == ViewType.grid ? ViewType.list : ViewType.grid; }); } }