385 lines
12 KiB
Dart
Raw Normal View History

2025-08-13 16:11:04 +07:00
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
2025-08-17 12:50:10 +07:00
import 'package:flutter_bloc/flutter_bloc.dart';
2025-08-13 16:11:04 +07:00
import 'package:line_icons/line_icons.dart';
import 'package:shimmer/shimmer.dart';
2025-08-13 16:11:04 +07:00
2025-08-17 12:50:10 +07:00
import '../../../application/category/category_loader/category_loader_bloc.dart';
2025-08-17 14:18:10 +07:00
import '../../../application/product/product_loader/product_loader_bloc.dart';
2025-08-13 16:11:04 +07:00
import '../../../common/theme/theme.dart';
2025-08-17 12:50:10 +07:00
import '../../../domain/category/category.dart';
2025-08-17 14:18:10 +07:00
import '../../../domain/product/product.dart';
2025-08-17 12:50:10 +07:00
import '../../../injection.dart';
2025-08-16 00:39:09 +07:00
import '../../components/appbar/appbar.dart';
2025-08-13 16:11:04 +07:00
import '../../components/button/button.dart';
2025-08-17 14:18:10 +07:00
import '../../components/widgets/empty_widget.dart';
2025-08-13 16:11:04 +07:00
import 'widgets/category_delegate.dart';
import 'widgets/product_card.dart';
2025-08-13 16:11:04 +07:00
import 'widgets/product_tile.dart';
@RoutePage()
2025-08-17 12:50:10 +07:00
class ProductPage extends StatefulWidget implements AutoRouteWrapper {
2025-08-13 16:11:04 +07:00
const ProductPage({super.key});
@override
State<ProductPage> createState() => _ProductPageState();
2025-08-17 12:50:10 +07:00
@override
2025-08-17 14:18:10 +07:00
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<CategoryLoaderBloc>()..add(CategoryLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<ProductLoaderBloc>()
..add(ProductLoaderEvent.fetched(isRefresh: true)),
),
],
2025-08-17 12:50:10 +07:00
child: this,
);
2025-08-13 16:11:04 +07:00
}
2025-08-13 16:32:08 +07:00
enum ViewType { grid, list }
2025-08-13 16:11:04 +07:00
class _ProductPageState extends State<ProductPage>
with TickerProviderStateMixin {
2025-08-17 12:50:10 +07:00
Category selectedCategory = Category.addAllData();
2025-08-13 16:32:08 +07:00
ViewType currentViewType = ViewType.grid;
ScrollController scrollController = ScrollController();
2025-08-13 16:11:04 +07:00
@override
initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocListener<ProductLoaderBloc, ProductLoaderState>(
listenWhen: (previous, current) =>
previous.categoryId != current.categoryId,
listener: (context, state) {
context.read<ProductLoaderBloc>().add(
ProductLoaderEvent.fetched(isRefresh: true),
2025-08-17 14:18:10 +07:00
);
},
child: BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
builder: (context, state) {
return Scaffold(
backgroundColor: AppColor.background,
body: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification is ScrollEndNotification &&
scrollController.position.extentAfter == 0) {
context.read<ProductLoaderBloc>().add(
ProductLoaderEvent.fetched(),
);
return true;
}
return true;
},
child: CustomScrollView(
controller: scrollController,
slivers: [
_buildSliverAppBar(),
_buildCategoryFilter(),
_buildProductContent(state),
],
),
),
);
},
),
2025-08-13 16:11:04 +07:00
);
}
Widget _buildSliverAppBar() {
return SliverAppBar(
expandedHeight: 120.0,
floating: false,
pinned: true,
elevation: 0,
2025-08-16 00:39:09 +07:00
flexibleSpace: CustomAppBar(title: 'Produk'),
2025-08-13 16:11:04 +07:00
actions: [
ActionIconButton(onTap: () {}, icon: LineIcons.search),
2025-08-13 16:32:08 +07:00
ActionIconButton(
onTap: _toggleViewType,
icon: currentViewType == ViewType.grid
? LineIcons.list
: LineIcons.thLarge,
),
2025-08-13 16:11:04 +07:00
],
);
}
Widget _buildCategoryFilter() {
2025-08-17 12:50:10 +07:00
return BlocBuilder<CategoryLoaderBloc, CategoryLoaderState>(
builder: (context, state) {
if (state.isFetching && state.categories.isEmpty) {
return _buildCategoryShimmer();
}
2025-08-17 12:50:10 +07:00
return SliverPersistentHeader(
pinned: true,
delegate: ProductCategoryHeaderDelegate(
categories: state.categories,
selectedCategory: selectedCategory,
onCategoryChanged: (category) {
setState(() {
selectedCategory = category;
});
if (category.id == Category.addAllData().id) {
context.read<ProductLoaderBloc>().add(
ProductLoaderEvent.categoryIdChanged(''),
);
} else {
context.read<ProductLoaderBloc>().add(
ProductLoaderEvent.categoryIdChanged(category.id),
);
}
2025-08-17 12:50:10 +07:00
},
),
);
},
2025-08-13 16:11:04 +07:00
);
}
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',
),
);
2025-08-13 16:11:04 +07:00
}
2025-08-13 16:32:08 +07:00
return currentViewType == ViewType.grid
? _buildProductGrid(state.products)
: _buildProductList(state.products);
2025-08-13 16:32:08 +07:00
}
Widget _buildProductGridShimmer() {
2025-08-13 16:11:04 +07:00
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
2025-08-13 16:11:04 +07:00
),
);
}
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() {
2025-08-13 16:32:08 +07:00
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
2025-08-13 16:32:08 +07:00
),
);
}
Widget _buildProductListItemShimmer() {
2025-08-13 16:32:08 +07:00
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]!,
2025-08-13 16:32:08 +07:00
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
// Image shimmer
2025-08-13 16:32:08 +07:00
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white,
2025-08-13 16:32:08 +07:00
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(width: 12),
// Content shimmer
2025-08-13 16:32:08 +07:00
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.infinity,
height: 16,
color: Colors.white,
2025-08-13 16:32:08 +07:00
),
const SizedBox(height: 8),
Container(width: 120, height: 12, color: Colors.white),
const SizedBox(height: 12),
2025-08-13 16:32:08 +07:00
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(width: 100, height: 16, color: Colors.white),
2025-08-13 16:32:08 +07:00
Container(
width: 60,
height: 24,
2025-08-13 16:32:08 +07:00
decoration: BoxDecoration(
color: Colors.white,
2025-08-13 16:32:08 +07:00
borderRadius: BorderRadius.circular(12),
),
),
],
),
],
),
),
const SizedBox(width: 8),
// Action button shimmer
Container(width: 24, height: 24, color: Colors.white),
2025-08-13 16:32:08 +07:00
],
),
),
),
);
}
Widget _buildProductGrid(List<Product> 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<Product> 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),
),
2025-08-13 16:32:08 +07:00
);
}
void _toggleViewType() {
setState(() {
currentViewType = currentViewType == ViewType.grid
? ViewType.list
: ViewType.grid;
});
}
2025-08-13 16:11:04 +07:00
}