diff --git a/lib/views/dashboard/fragments/appointments_fragment.dart b/lib/views/dashboard/fragments/appointments_fragment.dart index 4a38717..752bf11 100644 --- a/lib/views/dashboard/fragments/appointments_fragment.dart +++ b/lib/views/dashboard/fragments/appointments_fragment.dart @@ -15,6 +15,7 @@ import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/filters_list.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart'; import 'package:provider/provider.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -70,10 +71,10 @@ class AppointmentsFragment extends StatelessWidget { } }, child: appointmentsVM.state == ViewState.busy - ? const Center(child: CircularProgressIndicator()) + ? const AppointmentsListShimmer(itemCount: 5) : appointmentsVM.myFilteredAppointmentsForCustomers.isEmpty ? AppState().getIsViewOnly - ? LocaleKeys.loginToViewAppointments.tr().toText(fontSize: 16, color: MyColors.lightTextColor) + ? LocaleKeys.loginToViewAppointments.tr().toText(fontSize: 16, color: MyColors.lightTextColor) : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/views/dashboard/fragments/branches_fragment.dart b/lib/views/dashboard/fragments/branches_fragment.dart index 6d30827..bde2e92 100644 --- a/lib/views/dashboard/fragments/branches_fragment.dart +++ b/lib/views/dashboard/fragments/branches_fragment.dart @@ -14,6 +14,7 @@ import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/branch_details_card.dart'; import 'package:mc_common_app/widgets/common_widgets/filters_list.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart'; import 'package:provider/provider.dart'; class BranchesFragment extends StatelessWidget { @@ -66,9 +67,7 @@ class BranchesFragment extends StatelessWidget { child: Container( width: double.infinity, child: appointmentsVM.state == ViewState.busy - ? const Center( - child: CircularProgressIndicator(), - ) + ? const BranchesListShimmer(itemCount: 5) : appointmentsVM.nearbyBranches.isEmpty ? Center( child: LocaleKeys.no_branch.tr().toText( diff --git a/lib/views/dashboard/fragments/home_fragment.dart b/lib/views/dashboard/fragments/home_fragment.dart index 4a4affa..878aa03 100644 --- a/lib/views/dashboard/fragments/home_fragment.dart +++ b/lib/views/dashboard/fragments/home_fragment.dart @@ -4,7 +4,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/models/general_models/widgets_models.dart'; +import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart'; @@ -15,11 +18,111 @@ import 'package:mc_common_app/views/appointments/widgets/common_appointment_slid import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/view_all_widget.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart'; import 'package:provider/provider.dart'; -class HomeFragment extends StatelessWidget { +class HomeFragment extends StatefulWidget { const HomeFragment({Key? key}) : super(key: key); + @override + State createState() => _HomeFragmentState(); +} + +class _HomeFragmentState extends State { + bool _isSearchingServices = false; + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + void _toggleSearch() { + setState(() { + _isSearchingServices = !_isSearchingServices; + if (!_isSearchingServices) { + _searchController.clear(); + _searchQuery = ''; + } + }); + } + + List _filterServices(List services) { + if (_searchQuery.isEmpty) { + return services; + } + return services.where((service) { + return service.title.toLowerCase().contains(_searchQuery.toLowerCase()); + }).toList(); + } + + Widget _buildServicesHeader() { + if (_isSearchingServices) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 21), + child: Row( + children: [ + Expanded( + child: Container( + height: 55, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!, width: 1), + ), + child: TextField( + controller: _searchController, + autofocus: true, + style: const TextStyle(fontSize: 16), + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: InputDecoration( + hintText: LocaleKeys.searchService.tr(), + hintStyle: TextStyle(fontSize: 16, color: Colors.grey[600]), + prefixIcon: Icon(Icons.search, size: 26, color: Colors.grey[700]), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + ), + ), + ), + ), + 14.width, + InkWell( + onTap: _toggleSearch, + child: Container( + height: 55, + width: 55, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!, width: 1), + ), + child: Icon(Icons.close, size: 26, color: Colors.grey[700]), + ), + ), + ], + ), + ); + } else { + return Row( + children: [ + Expanded( + child: LocaleKeys.services.tr().toUpperCase().toText(fontSize: 20, isUpperCase: true, fontWeight: FontWeight.w600), + ), + InkWell( + onTap: _toggleSearch, + child: const Icon(Icons.search, size: 28, color: MyColors.primaryColor), + ), + ], + ).horPaddingMain(); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -69,38 +172,64 @@ class HomeFragment extends StatelessWidget { ).horPaddingMain().toViewOnly(context, onTap: () { navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), - CommonAppointmentSliderWidget() - .toWhiteContainer( - width: double.infinity, - backgroundColor: Colors.transparent, - ) - .margin(left: 21, right: 21) - .toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); - }), + Consumer( + builder: (BuildContext context, AppointmentsVM appointmentVM, Widget? child) { + // Show shimmer when loading + if (appointmentVM.state == ViewState.busy) { + return const AppointmentSliderShimmer(); + } + // Show actual data when idle + return CommonAppointmentSliderWidget() + .toWhiteContainer( + width: double.infinity, + backgroundColor: Colors.transparent, + ) + .margin(left: 21, right: 21) + .toViewOnly(context, onTap: () { + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + }); + }, + ), // SERVICES SECTION 14.height, Consumer( builder: (BuildContext context, AppointmentsVM appointmentVM, Widget? child) { + // Show shimmer when loading if (appointmentVM.state == ViewState.busy) { - return Center(child: CircularProgressIndicator()); + return Column( + children: [ + _buildServicesHeader(), + 14.height, + const ServicesGridShimmer(), + ], + ); } + // Show actual data when idle if (appointmentVM.branchesCategoriesFilterOptions.isNotEmpty) { + // Filter services based on search query + final filteredServices = _filterServices(appointmentVM.branchesCategoriesFilterOptions); + return Column( children: [ - ViewAllWidget( - title: LocaleKeys.services.tr().toUpperCase(), - subTitle: '', - onSubtitleTapped: () {}, - ).horPaddingMain().toViewOnly(context, onTap: () { + _buildServicesHeader().toViewOnly(context, onTap: () { navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), - MyServicesCategoriesWidget(homeCategories: appointmentVM.branchesCategoriesFilterOptions).horPaddingMain() + 14.height, + filteredServices.isEmpty + ? Padding( + padding: const EdgeInsets.all(40), + child: LocaleKeys.noServiceFound.tr().toText( + fontSize: 16, + color: Colors.grey, + textAlign: TextAlign.center, + ), + ) + : MyServicesCategoriesWidget(homeCategories: filteredServices).horPaddingMain(), ], ); } - return SizedBox(); + return const SizedBox(); }, ), @@ -150,9 +279,22 @@ class HomeFragment extends StatelessWidget { // ADS SECTION Consumer( builder: (BuildContext context, AdVM adVM, Widget? child) { + // Show shimmer when loading if (adVM.state == ViewState.busy) { - return Center(child: CircularProgressIndicator()); + return Column( + children: [ + 15.height, + ViewAllWidget( + title: LocaleKeys.my_active_Ads.tr().toUpperCase(), + subTitle: LocaleKeys.view_all.tr(), + onSubtitleTapped: () {}, + ).horPaddingMain(), + 14.height, + const AdsListShimmer(itemCount: 2), + ], + ); } + // Show actual data when idle return Column( children: [ if (adVM.myActiveAdsForHome.isNotEmpty) ...[ @@ -173,7 +315,7 @@ class HomeFragment extends StatelessWidget { shouldShowAdStatus: true, isAdsFragment: false, adsList: adVM.myActiveAdsForHome, - scrollPhysics: NeverScrollableScrollPhysics(), + scrollPhysics: const NeverScrollableScrollPhysics(), hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, ).toViewOnly(context, onTap: () { navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); @@ -181,30 +323,31 @@ class HomeFragment extends StatelessWidget { ], ) ], - if (adVM.exploreAds.isNotEmpty) ...[ - Column( - children: [ - 15.height, - ViewAllWidget( - title: LocaleKeys.myRecommendedAds.tr().toUpperCase(), - subTitle: LocaleKeys.view_all.tr(), - onSubtitleTapped: () { - context.read().onNavbarTapped(3); - context.read().updateIsExploreAds(true); - context.read().applyFilterOnExploreAds(vehicleBrandId: 0); - }, - ).horPaddingMain(), - AdsListWidget( - isDraftAds: false, - shouldShowAdStatus: false, - adsList: adVM.exploreAds.length >= 3 ? adVM.exploreAds.take(3).toList() : adVM.exploreAds, - isAdsFragment: false, - scrollPhysics: NeverScrollableScrollPhysics(), - hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, - ), - ], - ) - ] + // RECOMMENDED ADS SECTION - COMMENTED OUT + // if (adVM.exploreAds.isNotEmpty) ...[ + // Column( + // children: [ + // 15.height, + // ViewAllWidget( + // title: LocaleKeys.myRecommendedAds.tr().toUpperCase(), + // subTitle: LocaleKeys.view_all.tr(), + // onSubtitleTapped: () { + // context.read().onNavbarTapped(3); + // context.read().updateIsExploreAds(true); + // context.read().applyFilterOnExploreAds(vehicleBrandId: 0); + // }, + // ).horPaddingMain(), + // AdsListWidget( + // isDraftAds: false, + // shouldShowAdStatus: false, + // adsList: adVM.exploreAds.length >= 3 ? adVM.exploreAds.take(3).toList() : adVM.exploreAds, + // isAdsFragment: false, + // scrollPhysics: NeverScrollableScrollPhysics(), + // hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, + // ), + // ], + // ) + // ] ], ); }, diff --git a/lib/views/dashboard/widgets/my_services_categories_widget.dart b/lib/views/dashboard/widgets/my_services_categories_widget.dart index 4963163..f9de4a2 100644 --- a/lib/views/dashboard/widgets/my_services_categories_widget.dart +++ b/lib/views/dashboard/widgets/my_services_categories_widget.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:mc_common_app/classes/app_state.dart'; +import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/models/general_models/widgets_models.dart'; +import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/view_models/appointments_view_model.dart'; import 'package:mc_common_app/view_models/dashboard_view_model_customer.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; @@ -14,45 +17,89 @@ class MyServicesCategoriesWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 140, - child: ListView.builder( - itemCount: homeCategories.length, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemBuilder: (BuildContext context, int index) { - FilterListModel category = homeCategories[index]; - if (category.id == 0) { - return SizedBox(); - } - return SizedBox( - width: 90, - child: Column( - children: [ - category.iconUrl.buildNetworkImage(height: 73, width: 73, fit: BoxFit.cover).toCircle(borderRadius: 100), - 9.height, + final isArabic = AppState().isArabic(context); + // Filter out categories with id == 0 + final filteredCategories = homeCategories.where((category) => category.id != 0).toList(); + + // Define background colors to cycle through + final List bgColors = [ + const Color(0xFFF3F4F6), // Light gray + const Color(0xFFFEF4E9), // Light cream/orange + const Color(0xFFE5EEE5), // Light green + const Color(0xFFF0EFF5), // Light purple + ]; + + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.9, + ), + itemCount: filteredCategories.length, + itemBuilder: (BuildContext context, int index) { + FilterListModel category = filteredCategories[index]; + // Cycle through colors based on index + Color bgColor = bgColors[index % bgColors.length]; + + return Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: bgColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + category.isCustom ? category.iconUrl.buildNetworkImage(height: 32, width: 32, fit: BoxFit.fill) : category.iconUrl.buildNetworkImage(height: 32, width: 32, fit: BoxFit.fill), + 10.height, + Flexible( + child: (isArabic ? category.titleN : category.title).toText( + fontSize: 16, + fontWeight: FontWeight.w600, + maxLines: 3, + overflow: TextOverflow.ellipsis, + height: 1.2, + ), + ), + if (category.description.isNotEmpty || category.descriptionN.isNotEmpty) ...[ + 6.height, Flexible( - child: category.title.toText( - fontSize: 14, - textAlign: TextAlign.center, - maxLines: 2, + child: (category.description).toText( + fontSize: 10, + fontWeight: FontWeight.w600, + maxLines: 3, overflow: TextOverflow.ellipsis, - letterSpacing: -0.84, - height: 18 / 14, + height: 1.2, ), ), ], - ), - ).onPress(() { - final appointmentVM = context.read(); - final dashboardCustomerVM = context.read(); - context.read().onNavbarTapped(0); - dashboardCustomerVM.onNavbarTapped(0); - appointmentVM.applyFilterOnBranches(index: index); - }); - }, - ), + 10.height, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [MyAssets.arrowRight.buildSvg()], + ), + ], + ), + ).onPress(() { + final appointmentVM = context.read(); + final dashboardCustomerVM = context.read(); + context.read().onNavbarTapped(0); + dashboardCustomerVM.onNavbarTapped(0); + // Use the original index from homeCategories for filtering + int originalIndex = homeCategories.indexOf(category); + appointmentVM.applyFilterOnBranches(index: originalIndex); + }); + }, ); } }