feat: product

This commit is contained in:
efrilm
2025-08-17 14:18:10 +07:00
parent 82c0eaf5fe
commit 7d24b3296d
22 changed files with 3779 additions and 183 deletions
+46 -179
View File
@@ -4,11 +4,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:line_icons/line_icons.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_tile.dart';
@@ -20,9 +23,18 @@ class ProductPage extends StatefulWidget implements AutoRouteWrapper {
State<ProductPage> createState() => _ProductPageState();
@override
Widget wrappedRoute(BuildContext context) => BlocProvider(
create: (context) =>
getIt<CategoryLoaderBloc>()..add(CategoryLoaderEvent.fetched()),
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) =>
getIt<CategoryLoaderBloc>()..add(CategoryLoaderEvent.fetched()),
),
BlocProvider(
create: (context) =>
getIt<ProductLoaderBloc>()
..add(ProductLoaderEvent.fetched(isRefresh: true)),
),
],
child: this,
);
}
@@ -34,109 +46,6 @@ class _ProductPageState extends State<ProductPage>
Category selectedCategory = Category.addAllData();
ViewType currentViewType = ViewType.grid;
// Sample product data
List<Product> products = [
Product(
id: '1',
name: 'Nasi Goreng Special',
price: 25000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '8',
name: 'Nasi Goreng',
price: 15000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '9',
name: 'Nasi Goreng Telor',
price: 18000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '10',
name: 'Mie Goreng ',
price: 18000,
category: 'Makanan',
stock: 50,
imageUrl: 'assets/images/nasi_goreng.jpg',
isActive: true,
),
Product(
id: '2',
name: 'Es Teh Manis',
price: 8000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '6',
name: 'Es Jeruk',
price: 10000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '7',
name: 'Es Kelapa',
price: 12000,
category: 'Minuman',
stock: 100,
imageUrl: 'assets/images/es_teh.jpg',
isActive: true,
),
Product(
id: '3',
name: 'Keripik Singkong',
price: 15000,
category: 'Snack',
stock: 25,
imageUrl: 'assets/images/keripik.jpg',
isActive: true,
),
Product(
id: '4',
name: 'Es Krim Vanilla',
price: 12000,
category: 'Dessert',
stock: 30,
imageUrl: 'assets/images/ice_cream.jpg',
isActive: false,
),
Product(
id: '5',
name: 'Ayam Bakar',
price: 35000,
category: 'Makanan',
stock: 20,
imageUrl: 'assets/images/ayam_bakar.jpg',
isActive: true,
),
];
List<Product> get filteredProducts {
return products.where((product) {
bool matchesCategory =
selectedCategory.name == 'Semua' ||
product.category == selectedCategory.id;
return matchesCategory;
}).toList();
}
@override
initState() {
super.initState();
@@ -144,16 +53,20 @@ class _ProductPageState extends State<ProductPage>
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
_buildCategoryFilter(),
_buildProductContent(),
_buildEmptyState(),
],
),
return BlocBuilder<ProductLoaderBloc, ProductLoaderState>(
builder: (context, state) {
return Scaffold(
backgroundColor: AppColor.background,
body: CustomScrollView(
slivers: [
_buildSliverAppBar(),
_buildCategoryFilter(),
_buildProductContent(state.products),
_buildEmptyState(state.products),
],
),
);
},
);
}
@@ -195,17 +108,17 @@ class _ProductPageState extends State<ProductPage>
);
}
Widget _buildProductContent() {
if (filteredProducts.isEmpty) {
Widget _buildProductContent(List<Product> products) {
if (products.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return currentViewType == ViewType.grid
? _buildProductGrid()
: _buildProductList();
? _buildProductGrid(products)
: _buildProductList(products);
}
Widget _buildProductGrid() {
Widget _buildProductGrid(List<Product> products) {
return SliverPadding(
padding: const EdgeInsets.all(16.0),
sliver: SliverGrid(
@@ -216,21 +129,21 @@ class _ProductPageState extends State<ProductPage>
mainAxisSpacing: 16.0,
),
delegate: SliverChildBuilderDelegate((context, index) {
final product = filteredProducts[index];
final product = products[index];
return ProductTile(product: product, onTap: () {});
}, childCount: filteredProducts.length),
}, childCount: products.length),
),
);
}
Widget _buildProductList() {
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 = filteredProducts[index];
final product = products[index];
return _buildProductListItem(product);
}, childCount: filteredProducts.length),
}, childCount: products.length),
),
);
}
@@ -300,7 +213,7 @@ class _ProductPageState extends State<ProductPage>
),
const SizedBox(height: 4),
Text(
product.category,
'',
style: TextStyle(fontSize: 12, color: AppColor.textLight),
),
const SizedBox(height: 8),
@@ -327,7 +240,7 @@ class _ProductPageState extends State<ProductPage>
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Stock: ${product.stock}',
'Stock: ',
style: TextStyle(
fontSize: 12,
color: product.isActive
@@ -370,62 +283,16 @@ class _ProductPageState extends State<ProductPage>
});
}
Widget _buildEmptyState() {
if (filteredProducts.isNotEmpty) {
Widget _buildEmptyState(List<Product> products) {
if (products.isNotEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
child: Container(
height: 300,
margin: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: AppColor.textLight,
),
const SizedBox(height: 16),
Text(
'Tidak ada produk ditemukan',
style: TextStyle(
color: AppColor.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'Coba ubah filter atau tambah produk baru',
style: TextStyle(color: AppColor.textLight, fontSize: 14),
textAlign: TextAlign.center,
),
],
),
child: EmptyWidget(
title: 'Tidak ada produk ditemukan',
message: 'Coba ubah filter atau tambah produk baru',
),
);
}
}
// Product Model
class Product {
final String id;
final String name;
final int price;
final String category;
final int stock;
final String imageUrl;
bool isActive;
Product({
required this.id,
required this.name,
required this.price,
required this.category,
required this.stock,
required this.imageUrl,
required this.isActive,
});
}
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import '../../../../common/theme/theme.dart';
import '../../../../domain/product/product.dart';
import '../../../components/spacer/spacer.dart';
import '../product_page.dart';
class ProductTile extends StatelessWidget {
final Product product;
@@ -156,7 +156,7 @@ class ProductTile extends StatelessWidget {
children: [
Flexible(
child: Text(
'Stok: ${product.stock}',
'Stok: ',
style: AppStyle.xs.copyWith(
color: AppColor.textSecondary,
fontSize: 9,
@@ -171,7 +171,7 @@ class ProductTile extends StatelessWidget {
borderRadius: BorderRadius.circular(3.0),
),
child: Text(
product.category,
'',
style: AppStyle.xs.copyWith(
color: product.isActive
? AppColor.primary