299 lines
9.2 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';
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_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;
2025-08-13 16:11:04 +07:00
@override
initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
2025-08-17 14:18:10 +07:00
return BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
builder: (context, state) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
_buildCategoryFilter(),
_buildProductContent(state.products),
_buildEmptyState(state.products),
],
),
);
},
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) {
return SliverPersistentHeader(
pinned: true,
delegate: ProductCategoryHeaderDelegate(
categories: state.categories,
selectedCategory: selectedCategory,
onCategoryChanged: (category) {
setState(() {
selectedCategory = category;
});
},
),
);
},
2025-08-13 16:11:04 +07:00
);
}
2025-08-17 14:18:10 +07:00
Widget _buildProductContent(List<Product> products) {
if (products.isEmpty) {
2025-08-13 16:11:04 +07:00
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
2025-08-13 16:32:08 +07:00
return currentViewType == ViewType.grid
2025-08-17 14:18:10 +07:00
? _buildProductGrid(products)
: _buildProductList(products);
2025-08-13 16:32:08 +07:00
}
2025-08-17 14:18:10 +07:00
Widget _buildProductGrid(List<Product> products) {
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) {
2025-08-17 14:18:10 +07:00
final product = products[index];
2025-08-13 16:11:04 +07:00
return ProductTile(product: product, onTap: () {});
2025-08-17 14:18:10 +07:00
}, childCount: products.length),
2025-08-13 16:11:04 +07:00
),
);
}
2025-08-17 14:18:10 +07:00
Widget _buildProductList(List<Product> products) {
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) {
2025-08-17 14:18:10 +07:00
final product = products[index];
2025-08-13 16:32:08 +07:00
return _buildProductListItem(product);
2025-08-17 14:18:10 +07:00
}, childCount: products.length),
2025-08-13 16:32:08 +07:00
),
);
}
Widget _buildProductListItem(Product product) {
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: InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
// Product Image
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: AppColor.background,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
product.imageUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: AppColor.background,
child: Icon(
Icons.image_outlined,
color: AppColor.textLight,
size: 32,
),
);
},
),
),
),
const SizedBox(width: 12),
// Product Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
2025-08-17 14:18:10 +07:00
'',
2025-08-13 16:32:08 +07:00
style: TextStyle(fontSize: 12, color: AppColor.textLight),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rp ${_formatPrice(product.price)}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.primary,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: product.isActive
? Colors.green.withOpacity(0.1)
: Colors.red.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
2025-08-17 14:18:10 +07:00
'Stock: ',
2025-08-13 16:32:08 +07:00
style: TextStyle(
fontSize: 12,
color: product.isActive
? Colors.green
: Colors.red,
fontWeight: FontWeight.w500,
),
),
),
],
),
],
),
),
// Action Button
IconButton(
onPressed: () {},
icon: const Icon(Icons.more_vert),
color: AppColor.textLight,
),
],
),
),
),
);
}
String _formatPrice(int price) {
return price.toString().replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
void _toggleViewType() {
setState(() {
currentViewType = currentViewType == ViewType.grid
? ViewType.list
: ViewType.grid;
});
}
2025-08-17 14:18:10 +07:00
Widget _buildEmptyState(List<Product> products) {
if (products.isNotEmpty) {
2025-08-13 16:11:04 +07:00
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
2025-08-17 14:18:10 +07:00
child: EmptyWidget(
title: 'Tidak ada produk ditemukan',
message: 'Coba ubah filter atau tambah produk baru',
2025-08-13 16:11:04 +07:00
),
);
}
}