add favorite

This commit is contained in:
Phuoc Nguyen
2025-11-18 11:23:07 +07:00
parent 192c322816
commit a5eb95fa64
25 changed files with 2506 additions and 978 deletions

View File

@@ -58,14 +58,15 @@ class FavoritesPage extends ConsumerWidget {
);
if (confirmed == true && context.mounted) {
// Clear all favorites
await ref.read(favoritesProvider.notifier).clearAll();
// TODO: Implement clear all functionality
// For now, we would need to remove each product individually
// or add a clearAll method to the repository
// Show snackbar
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa tất cả yêu thích'),
content: Text('Chức năng này đang được phát triển'),
duration: Duration(seconds: 2),
),
);
@@ -79,6 +80,9 @@ class FavoritesPage extends ConsumerWidget {
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
final favoriteCount = ref.watch(favoriteCountProvider);
// Track if we've loaded data at least once to prevent empty state flash
final hasLoadedOnce = favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
@@ -121,26 +125,148 @@ class FavoritesPage extends ConsumerWidget {
body: SafeArea(
child: favoriteProductsAsync.when(
data: (products) {
if (products.isEmpty) {
// IMPORTANT: Only show empty state after we've confirmed data loaded
// This prevents empty state flash during initial load
if (products.isEmpty && hasLoadedOnce) {
return const _EmptyState();
}
// If products is empty but we haven't loaded yet, show loading
if (products.isEmpty && !hasLoadedOnce) {
return const _LoadingState();
}
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(favoritesProvider);
ref.invalidate(favoriteProductsProvider);
// Use the new refresh method from AsyncNotifier
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: _FavoritesGrid(products: products),
);
},
loading: () => const _LoadingState(),
error: (error, stackTrace) => _ErrorState(
error: error,
onRetry: () {
ref.invalidate(favoritesProvider);
ref.invalidate(favoriteProductsProvider);
},
),
loading: () {
// IMPORTANT: Check for previous data first to prevent empty state flash
final previousValue = favoriteProductsAsync.hasValue
? favoriteProductsAsync.value
: null;
// If we have previous data, show it while loading new data
if (previousValue != null && previousValue.isNotEmpty) {
return Stack(
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: _FavoritesGrid(products: previousValue),
),
const Positioned(
top: 16,
left: 0,
right: 0,
child: Center(
child: Card(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 8),
Text('Đang tải...'),
],
),
),
),
),
),
],
);
}
// Check if we should show skeleton or empty state
// Use favoriteCount as a hint - if it's > 0, we likely have data coming
if (favoriteCount > 0) {
// Show skeleton loading for better UX
return const _LoadingState();
}
// No previous data and no favorites - show skeleton briefly
return const _LoadingState();
},
error: (error, stackTrace) {
// Check if we have previous data to show with error
final previousValue = favoriteProductsAsync.hasValue
? favoriteProductsAsync.value
: null;
if (previousValue != null && previousValue.isNotEmpty) {
// Show previous data with error message
return Stack(
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: _FavoritesGrid(products: previousValue),
),
Positioned(
top: 16,
left: 16,
right: 16,
child: Material(
color: AppColors.danger,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: AppColors.white,
size: 20,
),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Không thể tải dữ liệu mới',
style: TextStyle(color: AppColors.white),
),
),
TextButton(
onPressed: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: const Text(
'Thử lại',
style: TextStyle(
color: AppColors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
],
);
}
// No previous data, show full error state
return _ErrorState(
error: error,
onRetry: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
},
);
},
),
),
);
@@ -188,7 +314,7 @@ class _EmptyState extends StatelessWidget {
const SizedBox(height: AppSpacing.sm),
// Subtext
Text(
const Text(
'Thêm sản phẩm vào danh sách yêu thích để xem lại sau',
style: TextStyle(fontSize: 14.0, color: AppColors.grey500),
textAlign: TextAlign.center,
@@ -269,9 +395,9 @@ class _ShimmerCard extends StatelessWidget {
// Image placeholder
Expanded(
child: Container(
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: AppColors.grey100,
borderRadius: const BorderRadius.vertical(
borderRadius: BorderRadius.vertical(
top: Radius.circular(ProductCardSpecs.borderRadius),
),
),
@@ -347,11 +473,11 @@ class _ShimmerCard extends StatelessWidget {
///
/// Displayed when there's an error loading favorites.
class _ErrorState extends StatelessWidget {
final Object error;
final VoidCallback onRetry;
const _ErrorState({required this.error, required this.onRetry});
final Object error;
final Future<void> Function() onRetry;
@override
Widget build(BuildContext context) {
return Center(
@@ -428,10 +554,10 @@ class _ErrorState extends StatelessWidget {
///
/// Displays favorite products in a grid layout.
class _FavoritesGrid extends StatelessWidget {
final List<Product> products;
const _FavoritesGrid({required this.products});
final List<Product> products;
@override
Widget build(BuildContext context) {
return GridView.builder(