diff --git a/lib/core/common/widgets/product_shimmer.dart b/lib/core/common/widgets/product_shimmer.dart new file mode 100644 index 0000000..6c5fc6d --- /dev/null +++ b/lib/core/common/widgets/product_shimmer.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:t_store/core/utils/constants/colors.dart'; +import 'package:t_store/core/utils/constants/sizes.dart'; +import 'package:t_store/core/utils/helpers/helper_functions.dart'; + +class ProductShimmer extends StatelessWidget { + const ProductShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final dark = THelperFunctions.isDarkMode(context); + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: TSizes.gridViewSpacing, + crossAxisSpacing: TSizes.gridViewSpacing, + mainAxisExtent: 288, + ), + itemCount: 4, // Show 4 shimmer items while loading + itemBuilder: (_, __) => Container( + width: 180, + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(TSizes.productImageRadius), + color: dark ? TColors.darkerGrey : TColors.white, + ), + child: Shimmer.fromColors( + baseColor: dark ? Colors.grey[850]! : Colors.grey[300]!, + highlightColor: dark ? Colors.grey[700]! : Colors.grey[100]!, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image container shimmer + Container( + width: double.infinity, + height: 180, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(TSizes.productImageRadius), + ), + ), + const SizedBox(height: TSizes.spaceBtwItems), + // Title shimmer + Padding( + padding: const EdgeInsets.symmetric(horizontal: TSizes.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 12, + color: Colors.white, + ), + const SizedBox(height: TSizes.spaceBtwItems / 2), + // Brand shimmer + Container( + width: 100, + height: 10, + color: Colors.white, + ), + const SizedBox(height: TSizes.spaceBtwItems), + // Price and cart button shimmer + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60, + height: 15, + color: Colors.white, + ), + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/common/widgets/rounded_image.dart b/lib/core/common/widgets/rounded_image.dart index a7fe786..aee1497 100644 --- a/lib/core/common/widgets/rounded_image.dart +++ b/lib/core/common/widgets/rounded_image.dart @@ -1,6 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:t_store/core/common/view_models/rounded_image_view_model.dart'; +import 'package:t_store/core/utils/constants/colors.dart'; +import 'package:t_store/core/utils/helpers/helper_functions.dart'; class RoundedImage extends StatelessWidget { const RoundedImage({ @@ -10,6 +13,8 @@ class RoundedImage extends StatelessWidget { final RoundedImageModel roundedImageModel; @override Widget build(BuildContext context) { + final dark = THelperFunctions.isDarkMode(context); + return GestureDetector( onTap: roundedImageModel.onTap, child: Container( @@ -21,30 +26,55 @@ class RoundedImage extends StatelessWidget { color: roundedImageModel.backgroundColor, ), child: ClipRRect( - borderRadius: roundedImageModel.applyImageRadius - ? BorderRadius.circular(roundedImageModel.borderRadius) - : BorderRadius.zero, - child: roundedImageModel.isNetworkImage - ? CachedNetworkImage( - imageUrl: roundedImageModel.image, - placeholderFadeInDuration: Duration.zero, - placeholder: (context, url) => SizedBox( - width: roundedImageModel.width, - height: roundedImageModel.height - - ), - errorWidget: (context, url, error) => - const Icon(Icons.error), - color: roundedImageModel.overlayColor, - fit: roundedImageModel.fit, - ) - : Image( - image: AssetImage( - roundedImageModel.image, + borderRadius: roundedImageModel.applyImageRadius + ? BorderRadius.circular(roundedImageModel.borderRadius) + : BorderRadius.zero, + child: roundedImageModel.isNetworkImage + ? CachedNetworkImage( + imageUrl: roundedImageModel.image, + placeholderFadeInDuration: Duration.zero, + placeholder: (context, url) => Shimmer.fromColors( + baseColor: dark ? Colors.grey[850]! : Colors.grey[300]!, + highlightColor: + dark ? Colors.grey[700]! : Colors.grey[100]!, + child: Container( + width: roundedImageModel.width, + height: roundedImageModel.height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: roundedImageModel.applyImageRadius + ? BorderRadius.circular( + roundedImageModel.borderRadius) + : null, + ), + ), + ), + errorWidget: (context, url, error) => Container( + width: roundedImageModel.width, + height: roundedImageModel.height, + decoration: BoxDecoration( + color: dark ? TColors.darkerGrey : TColors.light, + borderRadius: roundedImageModel.applyImageRadius + ? BorderRadius.circular( + roundedImageModel.borderRadius) + : null, ), - color: roundedImageModel.overlayColor, - fit: roundedImageModel.fit, - )), + child: Icon( + Icons.error, + color: dark ? TColors.light : TColors.dark, + ), + ), + color: roundedImageModel.overlayColor, + fit: roundedImageModel.fit, + ) + : Image( + image: AssetImage( + roundedImageModel.image, + ), + color: roundedImageModel.overlayColor, + fit: roundedImageModel.fit, + ), + ), ), ); } diff --git a/lib/core/utils/exceptions/firebase_auth_exceptions.dart b/lib/core/utils/exceptions/firebase_auth_exceptions.dart index 6d220d3..2e188e1 100644 --- a/lib/core/utils/exceptions/firebase_auth_exceptions.dart +++ b/lib/core/utils/exceptions/firebase_auth_exceptions.dart @@ -79,14 +79,10 @@ class TFirebaseAuthException implements Exception { return 'The provided Cordova configuration is invalid.'; case 'app-deleted': return 'This instance of FirebaseApp has been deleted.'; - case 'user-disabled': - return 'The user account has been disabled.'; case 'user-token-mismatch': return 'The provided user\'s token has a mismatch with the authenticated user\'s user ID.'; case 'web-storage-unsupported': return 'Web storage is not supported or is disabled.'; - case 'invalid-credential': - return 'The supplied credential is invalid. Please check the credential and try again.'; case 'app-not-authorized': return 'The app is not authorized to use Firebase Authentication with the provided API key.'; case 'keychain-error': diff --git a/lib/core/utils/theme/widget_themes/checkbox_theme.dart b/lib/core/utils/theme/widget_themes/checkbox_theme.dart index c9826de..d58e770 100644 --- a/lib/core/utils/theme/widget_themes/checkbox_theme.dart +++ b/lib/core/utils/theme/widget_themes/checkbox_theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../../constants/colors.dart'; import '../../constants/sizes.dart'; @@ -8,16 +9,17 @@ class TCheckboxTheme { /// Customizable Light Text Theme static CheckboxThemeData lightCheckboxTheme = CheckboxThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(TSizes.xs)), - checkColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(TSizes.xs)), + checkColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return TColors.white; } else { return TColors.black; } }), - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return TColors.primary; } else { return Colors.transparent; @@ -27,16 +29,17 @@ class TCheckboxTheme { /// Customizable Dark Text Theme static CheckboxThemeData darkCheckboxTheme = CheckboxThemeData( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(TSizes.xs)), - checkColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(TSizes.xs)), + checkColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return TColors.white; } else { return TColors.black; } }), - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return TColors.primary; } else { return Colors.transparent; diff --git a/lib/features/shop/presentation/views/home_view.dart b/lib/features/shop/presentation/views/home_view.dart index 9941f4e..b318ae2 100644 --- a/lib/features/shop/presentation/views/home_view.dart +++ b/lib/features/shop/presentation/views/home_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shimmer/shimmer.dart'; import 'package:t_store/core/common/view_models/grid_layout_view_model.dart'; import 'package:t_store/core/common/view_models/section_heading_view_model.dart'; import 'package:t_store/core/common/widgets/section_heading.dart'; @@ -15,6 +16,144 @@ import 'package:t_store/features/shop/presentation/views/all_products_view.dart' import 'package:t_store/features/shop/presentation/widgets/home_header_section.dart'; import 'package:t_store/features/shop/presentation/widgets/promo_banner_carousel_slider.dart'; +class HomeViewShimmer extends StatelessWidget { + const HomeViewShimmer({super.key}); + + @override + Widget build(BuildContext context) { + final dark = THelperFunctions.isDarkMode(context); + return Shimmer.fromColors( + baseColor: dark ? Colors.grey[850]! : Colors.grey[300]!, + highlightColor: dark ? Colors.grey[700]! : Colors.grey[100]!, + child: Column( + children: [ + // Header Section Shimmer + Container( + height: 60, + padding: const EdgeInsets.all(TSizes.md), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(TSizes.cardRadiusLg), + ), + ), + const SizedBox(height: TSizes.spaceBtwSections), + + // Banner Carousel Shimmer + Container( + height: 180, + margin: const EdgeInsets.symmetric(horizontal: TSizes.defaultSpace), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(TSizes.cardRadiusLg), + ), + ), + const SizedBox(height: TSizes.spaceBtwSections), + + // Section Heading Shimmer + Padding( + padding: + const EdgeInsets.symmetric(horizontal: TSizes.defaultSpace), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 120, + height: 20, + color: Colors.white, + ), + Container( + width: 80, + height: 20, + color: Colors.white, + ), + ], + ), + ), + const SizedBox(height: TSizes.spaceBtwItems), + + // Grid Layout Shimmer + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: + const EdgeInsets.symmetric(horizontal: TSizes.defaultSpace), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: TSizes.gridViewSpacing, + crossAxisSpacing: TSizes.gridViewSpacing, + mainAxisExtent: 288, + ), + itemCount: 4, + itemBuilder: (_, __) => Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(TSizes.productImageRadius), + ), + child: Column( + children: [ + // Product Image Shimmer + Expanded( + flex: 6, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: + BorderRadius.circular(TSizes.productImageRadius), + ), + ), + ), + // Product Details Shimmer + Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.all(TSizes.sm), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 20, + color: Colors.white, + ), + const SizedBox(height: TSizes.spaceBtwItems / 2), + Container( + width: 100, + height: 16, + color: Colors.white, + ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: 60, + height: 20, + color: Colors.white, + ), + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + class HomeView extends StatelessWidget { const HomeView({super.key}); @@ -41,8 +180,11 @@ class HomeView extends StatelessWidget { THelperFunctions.navigateToScreen( context, BlocProvider( - create: (context) => getIt()..getSortedProducts(sortBy: 'rating', sortType: "desc"), - child: const AllProductsView()), + create: (context) => getIt() + ..getSortedProducts( + sortBy: 'rating', sortType: "desc"), + child: const AllProductsView(), + ), ); }, actionButtonTitle: "View All", @@ -56,16 +198,18 @@ class HomeView extends StatelessWidget { } if (state is ShopSortedProductsLoaded) { return GridLayout( - gridLayoutModel: GridLayoutModel( - itemCount: state.productsList.length, - itemBuilder: (context, index) { - return VerticalProductCard( - product: state.productsList[index]); - }, - mainAxisExtent: 288, - )); + gridLayoutModel: GridLayoutModel( + itemCount: state.productsList.length, + itemBuilder: (context, index) { + return VerticalProductCard( + product: state.productsList[index], + ); + }, + mainAxisExtent: 288, + ), + ); } - return const CircularProgressIndicator(); + return const HomeViewShimmer(); }, ), ], diff --git a/lib/features/shop/presentation/widgets/sortable_products.dart b/lib/features/shop/presentation/widgets/sortable_products.dart index 88d1cd4..468fe67 100644 --- a/lib/features/shop/presentation/widgets/sortable_products.dart +++ b/lib/features/shop/presentation/widgets/sortable_products.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:iconsax/iconsax.dart'; import 'package:t_store/core/common/view_models/grid_layout_view_model.dart'; +import 'package:t_store/core/common/widgets/product_shimmer.dart'; import 'package:t_store/core/common/widgets/vertical_product_card.dart'; import 'package:t_store/core/utils/constants/sizes.dart'; import 'package:t_store/features/auth/presentation/widgets/grid_layout.dart'; @@ -21,7 +22,9 @@ class SortableProducts extends StatelessWidget { prefixIcon: Icon(Iconsax.sort), ), value: context.read().sortBy, - items:context.read().sortByList + items: context + .read() + .sortByList .map((e) => DropdownMenuItem( value: e, child: Text(e), @@ -32,7 +35,6 @@ class SortableProducts extends StatelessWidget { BlocProvider.of(context).getSortedProducts( sortBy: value.toString(), sortType: "desc", - ) }, ), @@ -40,7 +42,6 @@ class SortableProducts extends StatelessWidget { height: TSizes.spaceBtwSections, ), BlocBuilder(builder: (context, state) { - if (state is ShopSortedProductsLoaded) { return GridLayout( gridLayoutModel: GridLayoutModel( @@ -55,7 +56,7 @@ class SortableProducts extends StatelessWidget { return Text(state.error.message); } - return const CircularProgressIndicator(); + return const ProductShimmer(); }), ], ); diff --git a/pubspec.lock b/pubspec.lock index 26da299..81e87cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -824,6 +824,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 215831d..fe7daa3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: smooth_page_indicator: url_launcher: flutter_launcher_icons: + shimmer: ^3.0.0 dev_dependencies: flutter_test: