diff --git a/lib/core/constants/app_locale_keys.dart b/lib/core/constants/app_locale_keys.dart index c752eb1..c5ed397 100644 --- a/lib/core/constants/app_locale_keys.dart +++ b/lib/core/constants/app_locale_keys.dart @@ -1,6 +1,7 @@ abstract class AppLocaleKeys { ///Storage keys static const String language = 'language'; + static const String browseSearchHistory = 'browse-search-history'; static const String fontBold = "fontBold"; static const String fontMedium = "fontMedium"; diff --git a/lib/core/di/injection_container.config.dart b/lib/core/di/injection_container.config.dart index 5d4f2cf..d6ffb51 100644 --- a/lib/core/di/injection_container.config.dart +++ b/lib/core/di/injection_container.config.dart @@ -13,6 +13,8 @@ import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; import '../../feature/basket/presentation/blocs/basket_bloc.dart' as _i728; +import '../../feature/browse/presentation/blocs/browse_bloc/browse_bloc.dart' + as _i991; import '../../feature/common/presentation/blocs/language_bloc/language_bloc.dart' as _i942; import '../../feature/home/presentation/blocs/home_bloc/home_bloc.dart' @@ -33,9 +35,10 @@ extension GetItInjectableX on _i174.GetIt { }) { final gh = _i526.GetItHelper(this, environment, environmentFilter); gh.factory<_i1007.HomeBloc>(() => _i1007.HomeBloc()); + gh.factory<_i728.BasketBloc>(() => _i728.BasketBloc()); + gh.factory<_i991.BrowseBloc>(() => _i991.BrowseBloc()); gh.factory<_i580.MainBloc>(() => _i580.MainBloc()); gh.factory<_i311.SplashBloc>(() => _i311.SplashBloc()); - gh.factory<_i728.BasketBloc>(() => _i728.BasketBloc()); gh.singleton<_i306.StorageService>(() => _i306.StorageService()); gh.singleton<_i152.AppRoutes>(() => _i152.AppRoutes()); gh.factory<_i942.LanguageBloc>( diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart index 6df56ae..6bc660b 100644 --- a/lib/core/services/storage_service.dart +++ b/lib/core/services/storage_service.dart @@ -12,6 +12,16 @@ class StorageService { _sharedPreference.setString(key, value); } + void setStringList({required String key, required String value}) { + final oldList = _sharedPreference.getStringList(key); + final newList = oldList?..add(value); + _sharedPreference.setStringList(key, newList ?? []); + } + + List getStringList({required String key}) { + return _sharedPreference.getStringList(key) ?? []; + } + String? getString({required String key}) { return _sharedPreference.getString(key); } diff --git a/lib/feature/browse/browse.dart b/lib/feature/browse/browse.dart index 52503b4..5de6109 100644 --- a/lib/feature/browse/browse.dart +++ b/lib/feature/browse/browse.dart @@ -1,3 +1,10 @@ export 'presentation/pages/browse_page/browse_page.dart'; export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_browse_body.dart'; export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_browse_item.dart'; +export "presentation/blocs/browse_bloc/browse_bloc.dart"; +export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_all_categories_skeletonizer.dart'; +export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_browse_skeletonizer.dart'; +export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart'; +export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart'; +export 'package:food_delivery_client/feature/common/presentation/widgets/app_text_form_field.dart'; +export 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_top_categories_skeletonizer.dart'; diff --git a/lib/feature/browse/presentation/blocs/browse_bloc/browse_bloc.dart b/lib/feature/browse/presentation/blocs/browse_bloc/browse_bloc.dart new file mode 100644 index 0000000..5cfee10 --- /dev/null +++ b/lib/feature/browse/presentation/blocs/browse_bloc/browse_bloc.dart @@ -0,0 +1,48 @@ +import 'package:food_delivery_client/food_delivery_client.dart'; + +part 'browse_event.dart'; + +part 'browse_state.dart'; + +part 'browse_bloc.freezed.dart'; + +@injectable +class BrowseBloc extends Bloc { + BrowseBloc() : super(const BrowseState()) { + on<_GetTopCategories>(_onGetTopCategories); + on<_GetAllCategories>(_onGetAllCategories); + on<_GetSearchHistory>(_onGetSearchHistory); + on<_FocusChanged>(_onFocusChanged); + } + + Future _onGetTopCategories( + _GetTopCategories event, + Emitter emit, + ) async { + emit(state.copyWith(topCategorySt: RequestStatus.loading)); + await Future.delayed(TimeDelayConst.duration3); + emit(state.copyWith(topCategorySt: RequestStatus.loaded)); + } + + Future _onGetAllCategories( + _GetAllCategories event, + Emitter emit, + ) async { + emit(state.copyWith(allCategorySt: RequestStatus.loading)); + await Future.delayed(TimeDelayConst.duration3); + emit(state.copyWith(allCategorySt: RequestStatus.loaded)); + } + + Future _onGetSearchHistory( + _GetSearchHistory event, + Emitter emit, + ) async { + emit(state.copyWith(searchHistorySt: RequestStatus.loading)); + await Future.delayed(TimeDelayConst.duration3); + emit(state.copyWith(searchHistorySt: RequestStatus.loading)); + } + + void _onFocusChanged(_FocusChanged event, Emitter emit) { + emit(state.copyWith(isActive: event.isActive ?? !(state.isActive))); + } +} diff --git a/lib/feature/browse/presentation/blocs/browse_bloc/browse_event.dart b/lib/feature/browse/presentation/blocs/browse_bloc/browse_event.dart new file mode 100644 index 0000000..1bf52b5 --- /dev/null +++ b/lib/feature/browse/presentation/blocs/browse_bloc/browse_event.dart @@ -0,0 +1,14 @@ +part of 'browse_bloc.dart'; + +@freezed +class BrowseEvent with _$BrowseEvent { + const factory BrowseEvent.started() = _Started; + + const factory BrowseEvent.getTopCategories() = _GetTopCategories; + + const factory BrowseEvent.getAllCategories() = _GetAllCategories; + + const factory BrowseEvent.getSearchHistory() = _GetSearchHistory; + + const factory BrowseEvent.focusChanged(bool? isActive) = _FocusChanged; +} diff --git a/lib/feature/browse/presentation/blocs/browse_bloc/browse_state.dart b/lib/feature/browse/presentation/blocs/browse_bloc/browse_state.dart new file mode 100644 index 0000000..9faf172 --- /dev/null +++ b/lib/feature/browse/presentation/blocs/browse_bloc/browse_state.dart @@ -0,0 +1,12 @@ +part of 'browse_bloc.dart'; + +@freezed +abstract class BrowseState with _$BrowseState { + const factory BrowseState({ + @Default(RequestStatus.initial) RequestStatus topCategorySt, + @Default(RequestStatus.initial) RequestStatus allCategorySt, + @Default(RequestStatus.initial) RequestStatus searchHistorySt, + @Default([]) List searchHistory, + @Default(false) bool isActive, + }) = _BrowseState; +} diff --git a/lib/feature/browse/presentation/pages/browse_page/browse_page.dart b/lib/feature/browse/presentation/pages/browse_page/browse_page.dart index 9829d68..62960bb 100644 --- a/lib/feature/browse/presentation/pages/browse_page/browse_page.dart +++ b/lib/feature/browse/presentation/pages/browse_page/browse_page.dart @@ -6,8 +6,11 @@ class BrowsePage extends StatelessWidget { @override Widget build(BuildContext context) { - return WLayout( - child: Scaffold(body:WBrowseBody() ), + return BlocProvider( + create: (context) => sl() + ..add(BrowseEvent.getTopCategories()) + ..add(BrowseEvent.getAllCategories()), + child: WLayout(child: Scaffold(body: WBrowseBody())), ); } } diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart index 998c11d..72d4f44 100644 --- a/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart @@ -1,8 +1,25 @@ + import '../../../../../../food_delivery_client.dart'; class WAllCategories extends StatelessWidget { const WAllCategories({super.key}); + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.allCategorySt.isLoaded()) { + return WAllCategoriesBody(); + } + return WAllCategoriesSkeletonizer(); + }, + ); + } +} + +class WAllCategoriesBody extends StatelessWidget { + const WAllCategoriesBody({super.key}); + @override Widget build(BuildContext context) { return Column( diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories_skeletonizer.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories_skeletonizer.dart new file mode 100644 index 0000000..084bed6 --- /dev/null +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_all_categories_skeletonizer.dart @@ -0,0 +1,41 @@ + +import '../../../../../../food_delivery_client.dart'; + +class WAllCategoriesSkeletonizer extends StatelessWidget { + const WAllCategoriesSkeletonizer({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 25.verticalSpace, + Text( + context.loc.allCategories, + style: AppTextStyles.size24Medium.copyWith(height: 36 / 24), + ), + 11.verticalSpace, + GridView.builder( + itemCount: 10, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 15, + crossAxisSpacing: 10, + mainAxisExtent: 140, + ), + itemBuilder: (context, index) { + return WBrowseSkeletonizerItem(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_body.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_body.dart index 04847d9..01e85e1 100644 --- a/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_body.dart +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_body.dart @@ -1,43 +1,117 @@ -import 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_all_categories.dart'; -import 'package:food_delivery_client/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart'; -import 'package:food_delivery_client/feature/common/presentation/widgets/app_text_form_field.dart'; - import '../../../../../../food_delivery_client.dart'; -class WBrowseBody extends StatelessWidget { +class WBrowseBody extends StatefulWidget { const WBrowseBody({super.key}); + @override + State createState() => _WBrowseBodyState(); +} + +class _WBrowseBodyState extends State { + late FocusNode _focusNode; + late TextEditingController _controller; + + @override + void initState() { + _controller = TextEditingController(); + _focusNode = FocusNode(); + _focusNode.addListener(() { + context.read().add(BrowseEvent.focusChanged(true)); + }); + super.initState(); + } + + void listener() { + _focusNode.addListener(() { + log("${_focusNode.hasFocus}"); + setState(() {}); + }); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DecoratedBox( - decoration: BoxDecoration(color: AppColors.cFFFFFF), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 10.verticalSpace, - AppTextFormField( - controller: TextEditingController(), - prefixIcon: SvgPicture.asset(AppIcons.icSearch), - hintText: context.loc.categoriesShort, - hintTextStyle: AppTextStyles.size16Medium.copyWith( - color: AppColors.c660000 + return BlocBuilder( + builder: (context, state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DecoratedBox( + decoration: BoxDecoration(color: AppColors.cFFFFFF), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 10.verticalSpace, + Row( + spacing: 15, + children: [ +/* + InkWell( + onTap: () {}, + borderRadius: AppUtils.kBorderRadius25, + child: Ink( + height: 44, + width: 44, + decoration: BoxDecoration( + color: AppColors.cEEEEEE, + borderRadius: AppUtils.kBorderRadius25, + ), + child: SizedBox( + height: 44, + width: 44, + child: SvgPicture.asset( + AppIcons.icBack, + height: 20, + width: 20, + ).paddingAll(10), + ), + ), + ), +*/ + Expanded( + child: AppTextFormField( + focusNode: _focusNode, + controller: _controller, + prefixIcon: SvgPicture.asset(AppIcons.icSearch), + hintText: context.loc.categoriesShort, + hintTextStyle: AppTextStyles.size16Medium.copyWith( + color: AppColors.c660000, + ), + ), + ), + ], + ), + 15.verticalSpace, + ], + ), + ), + Expanded( + child: RefreshIndicator.adaptive( + onRefresh: () async { + context.read() + ..add(BrowseEvent.getTopCategories()) + ..add(BrowseEvent.getAllCategories()); + }, + child: SingleChildScrollView( + child: Column( + children: [ + WTopCategories(), + WAllCategories(), + 40.verticalSpace, + ], + ), ), ), - 15.verticalSpace, - ], - ), - ), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [WTopCategories(), WAllCategories(), 40.verticalSpace], ), - ), - ), - ], + ], + ); + }, ).paddingSymmetric(horizontal: 16); } } diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_skeletonizer.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_skeletonizer.dart new file mode 100644 index 0000000..4d9e4b7 --- /dev/null +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_browse_skeletonizer.dart @@ -0,0 +1,52 @@ +import '../../../../../../food_delivery_client.dart'; + +class WBrowseSkeletonizerItem extends StatelessWidget { + const WBrowseSkeletonizerItem({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Bounceable( + onTap: () {}, + child: Ink( + decoration: BoxDecoration( + color: AppColors.cFFFFFF, + borderRadius: AppUtils.kBorderRadius15, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: AppUtils.kBorderRadiusTop15, + child: CachedNetworkImage( + imageUrl: AppLocaleKeys.foodImageUrl, + width: context.w, + height: 96, + fit: BoxFit.cover, + ), + ), + SizedBox( + width: context.w, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: AppUtils.kBorderRadiusBottom15, + border: Border.all( + color: AppColors.cE8E8E8, + width: 1, + ), + ), + child: Text( + "Restaurant Rewards", + textAlign: TextAlign.center, + style: AppTextStyles.size16Regular.copyWith( + height: 20 / 16, + ), + ).paddingSymmetric(horizontal: 30), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart index 12b1db4..b6a1683 100644 --- a/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories.dart @@ -4,18 +4,34 @@ import '../../../../../../food_delivery_client.dart'; class WTopCategories extends StatelessWidget { const WTopCategories({super.key}); + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.topCategorySt.isLoaded()) { + return WTopCategoriesBody(); + } + return WTopCategoriesSkeletonizer(); + }, + ); + } +} + +class WTopCategoriesBody extends StatelessWidget { + const WTopCategoriesBody({super.key}); + @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.loc.topCategories, + context.loc.topCategories, style: AppTextStyles.size24Medium.copyWith(height: 36 / 24), ), 11.verticalSpace, GridView.builder( - itemCount:6, + itemCount: 6, shrinkWrap: true, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), diff --git a/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories_skeletonizer.dart b/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories_skeletonizer.dart new file mode 100644 index 0000000..54c1041 --- /dev/null +++ b/lib/feature/browse/presentation/pages/browse_page/widgets/w_top_categories_skeletonizer.dart @@ -0,0 +1,40 @@ + +import '../../../../../../food_delivery_client.dart'; + +class WTopCategoriesSkeletonizer extends StatelessWidget { + const WTopCategoriesSkeletonizer({super.key}); + + @override + Widget build(BuildContext context) { + return Skeletonizer( + enabled: true, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.loc.topCategories, + style: AppTextStyles.size24Medium.copyWith(height: 36 / 24), + ), + 11.verticalSpace, + GridView.builder( + itemCount: 6, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 15, + crossAxisSpacing: 10, + mainAxisExtent: 140, + ), + itemBuilder: (context, index) { + return WBrowseSkeletonizerItem(); + }, + ), + ], + ), + ); + } +} + + diff --git a/lib/feature/common/presentation/widgets/app_text_form_field.dart b/lib/feature/common/presentation/widgets/app_text_form_field.dart index 81d9c7c..5a93406 100644 --- a/lib/feature/common/presentation/widgets/app_text_form_field.dart +++ b/lib/feature/common/presentation/widgets/app_text_form_field.dart @@ -17,7 +17,7 @@ class AppTextFormField extends StatelessWidget { this.obscureText = false, required this.controller, this.prefixIcon, - this.prefixSvgPath, + this.prefixSvgPath, this.focusNode, }); final int? maxLines; @@ -36,6 +36,7 @@ class AppTextFormField extends StatelessWidget { final TextStyle? hintTextStyle; late final Widget? prefixIcon; final String? prefixSvgPath; + final FocusNode? focusNode; @override Widget build(BuildContext context) { @@ -45,6 +46,7 @@ class AppTextFormField extends StatelessWidget { maxLines: maxLines ?? 1, minLines: minLines ?? 1, onChanged: onChanged, + focusNode: focusNode, inputFormatters: inputFormatters, keyboardType: keyBoardType, style: textStyle ?? AppTextStyles.size16Regular,