This commit is contained in:
Phuoc Nguyen
2025-10-10 16:38:07 +07:00
parent e5b247d622
commit b94c158004
177 changed files with 25080 additions and 152 deletions

View File

@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../domain/entities/product.dart';
import '../../../../shared/widgets/price_display.dart';
/// Product card widget
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
// TODO: Navigate to product details or add to cart
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: product.imageUrl != null
? CachedNetworkImage(
imageUrl: product.imageUrl!,
fit: BoxFit.cover,
width: double.infinity,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Icon(
Icons.image_not_supported,
size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
: Container(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Icon(
Icons.inventory_2_outlined,
size: 48,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context).textTheme.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
PriceDisplay(price: product.price),
if (product.stockQuantity < 5)
Text(
'Low stock: ${product.stockQuantity}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/filtered_products_provider.dart';
import 'product_card.dart';
import '../../../../core/widgets/empty_state.dart';
/// Product grid widget
class ProductGrid extends ConsumerWidget {
final ProductSortOption sortOption;
const ProductGrid({
super.key,
this.sortOption = ProductSortOption.nameAsc,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final filteredProducts = ref.watch(filteredProductsProvider);
// Apply sorting
final sortedProducts = _sortProducts(filteredProducts, sortOption);
if (sortedProducts.isEmpty) {
return const EmptyState(
message: 'No products found',
subMessage: 'Try adjusting your filters',
icon: Icons.inventory_2_outlined,
);
}
return LayoutBuilder(
builder: (context, constraints) {
// Determine grid columns based on width
int crossAxisCount = 2;
if (constraints.maxWidth > 1200) {
crossAxisCount = 4;
} else if (constraints.maxWidth > 800) {
crossAxisCount = 3;
}
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 0.75,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: sortedProducts.length,
itemBuilder: (context, index) {
return RepaintBoundary(
child: ProductCard(product: sortedProducts[index]),
);
},
);
},
);
}
List<dynamic> _sortProducts(List<dynamic> products, ProductSortOption option) {
final sorted = List.from(products);
switch (option) {
case ProductSortOption.nameAsc:
sorted.sort((a, b) => a.name.compareTo(b.name));
break;
case ProductSortOption.nameDesc:
sorted.sort((a, b) => b.name.compareTo(a.name));
break;
case ProductSortOption.priceAsc:
sorted.sort((a, b) => a.price.compareTo(b.price));
break;
case ProductSortOption.priceDesc:
sorted.sort((a, b) => b.price.compareTo(a.price));
break;
case ProductSortOption.newest:
sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
break;
case ProductSortOption.oldest:
sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
break;
}
return sorted;
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/products_provider.dart';
/// Product search bar widget
class ProductSearchBar extends ConsumerStatefulWidget {
const ProductSearchBar({super.key});
@override
ConsumerState<ProductSearchBar> createState() => _ProductSearchBarState();
}
class _ProductSearchBarState extends ConsumerState<ProductSearchBar> {
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Search products...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
ref.read(searchQueryProvider.notifier).setQuery('');
},
)
: null,
),
onChanged: (value) {
ref.read(searchQueryProvider.notifier).setQuery(value);
},
);
}
}

View File

@@ -0,0 +1,6 @@
// Product Feature Widgets
export 'product_card.dart';
export 'product_grid.dart';
export 'product_search_bar.dart';
// This file provides a central export point for all product widgets