-
Notifications
You must be signed in to change notification settings - Fork 24
Add API-backed liked items profile screen #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f4c8fdb
Add Firestore-backed liked items screen
CherryCIC c8b2228
Hydrate liked items from product snapshots
CherryCIC 75e9aa6
Align liked items bottom nav with current flags
CherryCIC e41b0a9
Merge remote-tracking branch 'origin/main' into cherry/liked-items-sc…
CherryCIC f89de33
Use product API for liked items
CherryCIC ba200dd
Merge branch 'main' into cherry/liked-items-screen-clean
CherryCIC ea39364
Merge branch 'main' into cherry/liked-items-screen-clean
CherryCIC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| import 'package:cherry_mvp/core/config/config.dart'; | ||
| import 'package:cherry_mvp/core/config/app_spacing.dart'; | ||
| import 'package:cherry_mvp/core/models/product.dart'; | ||
| import 'package:cherry_mvp/core/router/router.dart'; | ||
| import 'package:cherry_mvp/features/donation/donation_page.dart'; | ||
| import 'package:cherry_mvp/features/home/widgets/bottom_nav_bar.dart'; | ||
| import 'package:cherry_mvp/features/liked_items/liked_items_view_model.dart'; | ||
| import 'package:cherry_mvp/features/products/product_card.dart'; | ||
| import 'package:cherry_mvp/features/products/product_repository.dart'; | ||
| import 'package:cherry_mvp/features/products/product_viewmodel.dart'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:provider/provider.dart'; | ||
|
|
||
| class LikedItemsPage extends StatelessWidget { | ||
| const LikedItemsPage({super.key}); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return ChangeNotifierProvider<LikedItemsViewModel>( | ||
| create: (context) => LikedItemsViewModel( | ||
| productRepository: context.read<ProductRepository>(), | ||
| productViewModel: context.read<ProductViewModel>(), | ||
| )..loadLikedProducts(), | ||
| child: const _LikedItemsView(), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class _LikedItemsView extends StatelessWidget { | ||
| const _LikedItemsView(); | ||
|
|
||
| static const int _homeNavIndex = 0; | ||
| static const int _inboxNavIndex = 1; | ||
| static const int _giveNavIndex = FeatureFlags.showInbox ? 2 : 1; | ||
| static const int _searchNavIndex = _giveNavIndex + 1; | ||
| static const int _profileNavIndex = _giveNavIndex + (FeatureFlags.showSearchNavigation ? 2 : 1); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return Scaffold( | ||
| appBar: AppBar( | ||
| leading: const BackButton(), | ||
| centerTitle: true, | ||
| title: const Text(AppStrings.likedItemsTitle), | ||
| ), | ||
| body: SafeArea( | ||
| child: Consumer<LikedItemsViewModel>( | ||
| builder: (context, viewModel, _) { | ||
| switch (viewModel.status) { | ||
| case LikedItemsStatus.initial: | ||
| case LikedItemsStatus.loading: | ||
| return const Center(child: CircularProgressIndicator()); | ||
| case LikedItemsStatus.empty: | ||
| return const _LikedItemsEmptyState(); | ||
| case LikedItemsStatus.error: | ||
| return _LikedItemsErrorState( | ||
| message: viewModel.errorMessage ?? AppStrings.likedItemsLoadError, | ||
| onRetry: viewModel.retry, | ||
| ); | ||
| case LikedItemsStatus.loaded: | ||
| return _LikedProductsGrid(products: viewModel.products); | ||
| } | ||
| }, | ||
| ), | ||
| ), | ||
| bottomNavigationBar: CherryBottomNavBar( | ||
| selectedIndex: _profileNavIndex, | ||
| onItemSelected: (index) => _handleNavTap(context, index), | ||
| selectedColor: Theme.of(context).colorScheme.primary, | ||
| unselectedColor: Theme.of(context).colorScheme.secondary, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| void _handleNavTap(BuildContext context, int index) { | ||
| final navigator = context.read<NavigationProvider>(); | ||
|
|
||
| if (index == _homeNavIndex) { | ||
| navigator.navigateToAndRemoveUntil(AppRoutes.home, (_) => false); | ||
| return; | ||
| } | ||
|
|
||
| if (FeatureFlags.showInbox && index == _inboxNavIndex) { | ||
| navigator.navigateToAndRemoveUntil(AppRoutes.home, (_) => false); | ||
| return; | ||
| } | ||
|
|
||
| if (index == _giveNavIndex) { | ||
| showDialog( | ||
| context: context, | ||
| builder: (context) => const Dialog.fullscreen(child: DonationPage()), | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (FeatureFlags.showSearchNavigation && index == _searchNavIndex) { | ||
| context.read<SearchController>().openView(); | ||
| return; | ||
| } | ||
|
|
||
| if (index == _profileNavIndex && Navigator.of(context).canPop()) { | ||
| Navigator.of(context).pop(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class _LikedProductsGrid extends StatelessWidget { | ||
| const _LikedProductsGrid({required this.products}); | ||
|
|
||
| final List<Product> products; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final size = MediaQuery.sizeOf(context); | ||
| final textScale = MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.4); | ||
| final cardWidth = (size.width - 24 - 12) / 2; | ||
| final imageHeight = cardWidth / AppSpacing.imageContainerAspectRatio; | ||
| final cardHeight = imageHeight + (120.0 * textScale); | ||
|
|
||
| return GridView.builder( | ||
| padding: const EdgeInsets.fromLTRB(12, 12, 12, 24), | ||
| itemCount: products.length, | ||
| gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( | ||
| crossAxisCount: 2, | ||
| mainAxisSpacing: 24, | ||
| crossAxisSpacing: 12, | ||
| mainAxisExtent: cardHeight, | ||
| ), | ||
| itemBuilder: (context, index) { | ||
| final product = products[index]; | ||
|
|
||
| return ProductCard( | ||
| key: ValueKey(product.id), | ||
| product: product, | ||
| onTap: () => context.read<ProductViewModel>().goToProductPage(product), | ||
| onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product), | ||
| ); | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class _LikedItemsEmptyState extends StatelessWidget { | ||
| const _LikedItemsEmptyState(); | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return Center( | ||
| child: Padding( | ||
| padding: const EdgeInsets.all(32), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| Icon( | ||
| Icons.favorite_border, | ||
| size: 48, | ||
| color: Theme.of(context).colorScheme.primary, | ||
| ), | ||
| const SizedBox(height: 16), | ||
| Text( | ||
| AppStrings.likedItemsEmptyTitle, | ||
| textAlign: TextAlign.center, | ||
| style: Theme.of(context).textTheme.titleLarge, | ||
| ), | ||
| const SizedBox(height: 8), | ||
| Text( | ||
| AppStrings.likedItemsEmptyBody, | ||
| textAlign: TextAlign.center, | ||
| style: Theme.of(context).textTheme.bodyMedium, | ||
| ), | ||
| const SizedBox(height: 24), | ||
| FilledButton( | ||
| onPressed: () => | ||
| context.read<NavigationProvider>().navigateToAndRemoveUntil(AppRoutes.home, (_) => false), | ||
| child: const Text(AppStrings.likedItemsBrowseProducts), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class _LikedItemsErrorState extends StatelessWidget { | ||
| const _LikedItemsErrorState({ | ||
| required this.message, | ||
| required this.onRetry, | ||
| }); | ||
|
|
||
| final String message; | ||
| final VoidCallback onRetry; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return Center( | ||
| child: Padding( | ||
| padding: const EdgeInsets.all(32), | ||
| child: Column( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| Icon( | ||
| Icons.error_outline, | ||
| size: 48, | ||
| color: Theme.of(context).colorScheme.error, | ||
| ), | ||
| const SizedBox(height: 16), | ||
| Text( | ||
| message, | ||
| textAlign: TextAlign.center, | ||
| style: Theme.of(context).textTheme.titleMedium, | ||
| ), | ||
| const SizedBox(height: 24), | ||
| OutlinedButton( | ||
| onPressed: onRetry, | ||
| child: const Text(AppStrings.retry), | ||
| ), | ||
| ], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle the potential failure when unliking a product.
Currently, if
unlikeProductfails (e.g., due to a network error), it returnsfalseand sets anerrorMessagein the view model. However, because the pagestatusremainsLikedItemsStatus.loaded, the UI ignores this error silently and leaves the user wondering why the item wasn't removed.Please await the result and display a
SnackBarto inform the user if the action fails.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents