From 5144f1c4462aea8378b906c2ba8d3ca726ea695d Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Wed, 19 Feb 2025 17:12:49 +0330 Subject: [PATCH] categories handle --- analysis_options.yaml | 1 + cubit/categories_cubit.dart | 1 + lib/core/routes/app_router.dart | 22 +- lib/core/services/api/api_service.dart | 6 +- lib/core/services/api/response_model.dart | 7 +- .../models/categories/categories_model.dart | 34 ++- .../repositories/category_repository.dart | 38 ++- lib/main.dart | 9 +- .../cubit/categories_children_cubit.dart | 38 +++ .../cubit/categories_children_state.dart | 60 ++++ .../category/cubit/categories_cubit.dart | 38 +++ .../cubit/categories_state.dart | 27 +- .../category/cubit/category_cubit.dart | 19 ++ .../category/cubit/category_state.dart | 19 ++ .../providers/cubit/categories_cubit.dart | 27 -- .../{cubit => }/them_mode_cubit.dart | 0 .../{cubit => }/user_info_cubit.dart | 0 .../{cubit => }/user_info_state.dart | 0 .../ui/screens/auth/auth_page.dart | 2 +- .../screens/categories/categories_page.dart | 124 ++++++++ .../ui/screens/home/home_page.dart | 148 +++++----- .../ui/screens/home/home_screen.dart | 140 ++++++---- lib/presentation/ui/theme/custom_colors.dart | 17 +- lib/presentation/ui/theme/theme.dart | 4 + .../ui/widgets/default_placeholder.dart | 34 +++ .../ui/widgets/dialog/image_gallary.dart | 12 - .../ui/widgets/hover_submenu.dart | 89 ++++++ .../navigations/categories_mega_menu.dart | 257 +++++++++++++++++ .../widgets/navigations/drop_down_demo2.dart | 264 ++++++++++++++++++ .../ui/widgets/navigations/mega_menu.dart | 44 +++ pubspec.lock | 16 ++ pubspec.yaml | 2 + 32 files changed, 1304 insertions(+), 195 deletions(-) create mode 100644 cubit/categories_cubit.dart create mode 100644 lib/presentation/providers/category/cubit/categories_children_cubit.dart create mode 100644 lib/presentation/providers/category/cubit/categories_children_state.dart create mode 100644 lib/presentation/providers/category/cubit/categories_cubit.dart rename lib/presentation/providers/{ => category}/cubit/categories_state.dart (57%) create mode 100644 lib/presentation/providers/category/cubit/category_cubit.dart create mode 100644 lib/presentation/providers/category/cubit/category_state.dart delete mode 100644 lib/presentation/providers/cubit/categories_cubit.dart rename lib/presentation/providers/{cubit => }/them_mode_cubit.dart (100%) rename lib/presentation/providers/{cubit => }/user_info_cubit.dart (100%) rename lib/presentation/providers/{cubit => }/user_info_state.dart (100%) create mode 100644 lib/presentation/ui/screens/categories/categories_page.dart create mode 100644 lib/presentation/ui/widgets/default_placeholder.dart create mode 100644 lib/presentation/ui/widgets/hover_submenu.dart create mode 100644 lib/presentation/ui/widgets/navigations/categories_mega_menu.dart create mode 100644 lib/presentation/ui/widgets/navigations/drop_down_demo2.dart create mode 100644 lib/presentation/ui/widgets/navigations/mega_menu.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index a5f616f..413d44a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,6 +10,7 @@ analyzer: errors: avoid_print: ignore + deprecated_member_use: ignore deprecated_member_use_from_same_package: ignore include: package:flutter_lints/flutter.yaml diff --git a/cubit/categories_cubit.dart b/cubit/categories_cubit.dart new file mode 100644 index 0000000..7072517 --- /dev/null +++ b/cubit/categories_cubit.dart @@ -0,0 +1 @@ +// This file is no longer needed and can be deleted. diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index fef02c5..12fc2f3 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -1,19 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:proxibuy/presentation/providers/cubit/user_info_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/category_cubit.dart'; +import 'package:proxibuy/presentation/providers/user_info_cubit.dart'; import 'package:proxibuy/presentation/ui/screens/auth/auth_page.dart'; +import 'package:proxibuy/presentation/ui/screens/categories/categories_page.dart'; import 'package:proxibuy/presentation/ui/screens/home/explore_screen.dart'; import 'package:proxibuy/presentation/ui/screens/home/home_page.dart'; import 'package:proxibuy/presentation/ui/screens/home/home_screen.dart'; import 'package:proxibuy/presentation/ui/screens/home/setting_screen.dart'; -import 'package:proxibuy/presentation/ui/screens/product/product_page.dart'; +import 'package:proxibuy/presentation/ui/widgets/navigations/drop_down_demo2.dart'; class AppRouter { static final initial = '/'; static final explore = '/explore'; static final setting = '/settings'; static final product = '/product'; + static final categories = '/categories'; static List home = [initial, explore, setting]; @@ -79,9 +82,20 @@ class AppRouter { GoRoute( path: product, builder: (BuildContext context, GoRouterState state) { - return ProductPage(); + return DropDownDemo3(); + }, + ), + GoRoute( + path: '$categories/:id', + builder: (BuildContext context, GoRouterState state) { + final id = state.pathParameters['id']!; + return BlocProvider( + create: (context) => CategoryCubit()..getCategory(id), + child: CategoriesPage( + id: id, + ), + ); }, - routes: [], ), ]), ]), diff --git a/lib/core/services/api/api_service.dart b/lib/core/services/api/api_service.dart index d66b2c1..6723fa9 100644 --- a/lib/core/services/api/api_service.dart +++ b/lib/core/services/api/api_service.dart @@ -10,8 +10,7 @@ class ApiService { late Dio _dio; void setAuthToken(String token) { - _dio.options.headers['Authorization'] = - 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJheC1zX3N3Tm5fU1hDTVkzWFowSDVKekNhQ0psVXh6bmZ0WHBxSk1YUEF3In0.eyJleHAiOjE3Mzk4ODcwNzksImlhdCI6MTczOTg4NTI3OSwianRpIjoiYTlmZGE4NzItZmJhZC00ZmQ5LTg3MTMtMTcwYjM3MWE1NTM2IiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5saWFyYS5ydW4vcmVhbG1zL2xiYSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmMzljODIxNi0zODhhLTQ0ZTEtODVhOC00Zjk5NmU2NmU2MDQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmcm9udGVuZCIsInNpZCI6IjIwNWNmNTBkLTE5MWUtNGViMS1iODBkLTMzMTFiNjIzYTZhMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9sYmEtYXBpLmxpYXJhLnJ1bi8qIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwLyoiLCIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1sYmEiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19LCJmcm9udGVuZCI6eyJyb2xlcyI6WyJzaG9wIiwidXNlciJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6ImRlbW8gZGVtbyIsInByZWZlcnJlZF91c2VybmFtZSI6ImRlbW8iLCJnaXZlbl9uYW1lIjoiZGVtbyIsImZhbWlseV9uYW1lIjoiZGVtbyIsImVtYWlsIjoiZGVtb0BnbWFpbC5jb20ifQ.r_RtuJLZ9HQZdoy1Fi29d7hbXqcQ497XThKUmAjd_ClenE2VjpTzpogTOFwrrzJqRxm9fDpBflliqBWRg7KwR7irxW4HHBBaJmCaLzLbG8EtiuvciwDc9ugqLwUvGs2Gnzc7P0RA9aWzVY2lBkDZa7GeEvCCI4k0pbSWsAjX-Ax2vHxCqW8GzMsBeLkQ2BE9cyKX5Q3f-yne5HjDoxF1350qPlXwxIRkmM2Ct-aiMm7CjCaawPdTqdritsWejwTtaVCQtzkHPyeToPwE_X1YLWDlFdwpWSzjiI2fDFyV-MykLgDZevxHtvFoIg-2f6Zsm2_t7AMoCr-tM7vquCZTaw'; + _dio.options.headers['Authorization'] = 'Bearer $token'; } ApiService() { @@ -28,7 +27,8 @@ class ApiService { )..interceptors.add(PrettyDioLogger( enabled: kDebugMode, )); - setAuthToken(''); + setAuthToken( + 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJheC1zX3N3Tm5fU1hDTVkzWFowSDVKekNhQ0psVXh6bmZ0WHBxSk1YUEF3In0.eyJleHAiOjE3Mzk5NzkwNDUsImlhdCI6MTczOTk0MzA0NSwianRpIjoiMzY2NDllZWQtNTBiNy00ZWYyLWIyNmUtOTcxZDUwODBhODc1IiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5saWFyYS5ydW4vcmVhbG1zL2xiYSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmMzljODIxNi0zODhhLTQ0ZTEtODVhOC00Zjk5NmU2NmU2MDQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmcm9udGVuZCIsInNpZCI6IjcwZDlhNDhmLTcxMDktNGYxNi1hNTQ5LTVmNjE3MDk4ZTJmMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9sYmEtYXBpLmxpYXJhLnJ1bi8qIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwLyoiLCIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1sYmEiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19LCJmcm9udGVuZCI6eyJyb2xlcyI6WyJzaG9wIiwidXNlciJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6ImRlbW8gZGVtbyIsInByZWZlcnJlZF91c2VybmFtZSI6ImRlbW8iLCJnaXZlbl9uYW1lIjoiZGVtbyIsImZhbWlseV9uYW1lIjoiZGVtbyIsImVtYWlsIjoiZGVtb0BnbWFpbC5jb20ifQ.H3MssiP94FiWyc7FqfoSK7Zt58VOZmunM1D-wrseDiTQF2lFIfCMYbWkKv1ko8hn-zn-1ExV_6auFCNS0C_wTyFbGq_IHwYQr9nGqji_cr4dum19doASpfZRQiR_oR5RM96Ht5lV3nY_X7o-ksJEiRDOHUZ-xmDLkxhGWfeTO90DXYWv_S39mS55R7SsQz7PI83B7ya9qgp-5GND_oY3iNjDYVTI46EQuGOTiNyLgUrRk64IFy4Bbhp-EVj7QhGwkDEOosAytzE5aqW98-1GUSfUS77P36Ln0olQEb_uYed8EDkdauAIPN-iN8Eg4q7QmT-fpBCP61dcy04FRfTzPw'); } /// 🔹 Handle GET requests diff --git a/lib/core/services/api/response_model.dart b/lib/core/services/api/response_model.dart index a6f5900..622389d 100644 --- a/lib/core/services/api/response_model.dart +++ b/lib/core/services/api/response_model.dart @@ -47,10 +47,9 @@ class Meta { currentPage = json['currentPage']; totalPages = json['totalPages']; if (json['sortBy'] != null) { - // sortBy = >[]; - // json['sortBy'].forEach((v) { - // sortBy!.addAll(v.toList()); - // }); + sortBy = (json['sortBy'] as List) + .map((item) => (item as List).map((e) => e.toString()).toList()) + .toList(); } } diff --git a/lib/data/models/categories/categories_model.dart b/lib/data/models/categories/categories_model.dart index 795dcb5..a25ff41 100644 --- a/lib/data/models/categories/categories_model.dart +++ b/lib/data/models/categories/categories_model.dart @@ -1,24 +1,56 @@ class CategoriesModel { String? id; + String? createdAt; + String? updatedAt; + String? state; String? name; int? count; + String? description; + String? createdBy; String? parentId; + String? icon; + String? url; - CategoriesModel({this.id, this.name, this.count, this.parentId}); + CategoriesModel( + {this.id, + this.createdAt, + this.updatedAt, + this.state, + this.name, + this.count, + this.description, + this.createdBy, + this.parentId, + this.icon, + this.url}); CategoriesModel.fromJson(Map json) { id = json['id']; + createdAt = json['createdAt']; + updatedAt = json['updatedAt']; + state = json['state']; name = json['name']; count = json['count']; + description = json['description']; + createdBy = json['created_by']; parentId = json['parent_id']; + icon = json['icon']; + url = json['url']; } Map toJson() { final Map data = {}; data['id'] = id; + data['createdAt'] = createdAt; + data['updatedAt'] = updatedAt; + data['state'] = state; data['name'] = name; data['count'] = count; + data['description'] = description; + data['created_by'] = createdBy; data['parent_id'] = parentId; + data['icon'] = icon; + data['url'] = url; return data; } } diff --git a/lib/data/repositories/category_repository.dart b/lib/data/repositories/category_repository.dart index 84a508c..d92fe5f 100644 --- a/lib/data/repositories/category_repository.dart +++ b/lib/data/repositories/category_repository.dart @@ -4,10 +4,10 @@ import 'package:proxibuy/core/services/api/response_model.dart'; import 'package:proxibuy/data/models/categories/categories_model.dart'; class CategoryRepository { - static Future?> fetchAll() async { + static Future>> fetchAll( + {int page = 1}) async { try { - var response = await apiService.get(ApiRoutes.category); - print("Users: $response"); + var response = await apiService.get('${ApiRoutes.category}?page=$page'); final res = ResponseModel>.fromJson( response, (data) { @@ -16,7 +16,37 @@ class CategoryRepository { .toList(); }, ); - return res.data; + return res; + } catch (e) { + print("Error: $e"); + rethrow; + } + } + + static Future>> fetchAllChild( + {int page = 1, required final String id}) async { + try { + var response = await apiService + .get('${ApiRoutes.category}?page=$page?filter.parent_id=$id'); + final res = ResponseModel>.fromJson( + response, + (data) { + return (data as List) + .map((item) => CategoriesModel.fromJson(item)) + .toList(); + }, + ); + return res; + } catch (e) { + print("Error: $e"); + rethrow; + } + } + + static Future fetchOne({required final String id}) async { + try { + var response = await apiService.get('${ApiRoutes.category}/$id'); + return CategoriesModel.fromJson(response['category']); } catch (e) { print("Error: $e"); rethrow; diff --git a/lib/main.dart b/lib/main.dart index d01890d..2f818a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,9 +4,10 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/utils/my_custom_scroll_behavior.dart'; import 'package:proxibuy/data/storage/shared_preferences_helper.dart'; -import 'package:proxibuy/presentation/providers/cubit/categories_cubit.dart'; -import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; -import 'package:proxibuy/presentation/providers/cubit/user_info_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_children_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_cubit.dart'; +import 'package:proxibuy/presentation/providers/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/providers/user_info_cubit.dart'; import 'package:proxibuy/presentation/ui/theme/theme.dart'; import 'package:url_strategy/url_strategy.dart'; @@ -19,6 +20,8 @@ void main() async { runApp(MultiBlocProvider( providers: [ BlocProvider(create: (context) => ThemModeCubit()), + BlocProvider( + create: (context) => CategoriesChildrenCubit()), BlocProvider(create: (context) => CategoriesCubit()), BlocProvider( create: (context) => UserInfoCubit()..getUserInfo()), diff --git a/lib/presentation/providers/category/cubit/categories_children_cubit.dart b/lib/presentation/providers/category/cubit/categories_children_cubit.dart new file mode 100644 index 0000000..e416860 --- /dev/null +++ b/lib/presentation/providers/category/cubit/categories_children_cubit.dart @@ -0,0 +1,38 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/data/models/categories/categories_model.dart'; +import 'package:proxibuy/data/repositories/category_repository.dart'; + +part 'categories_children_state.dart'; + +class CategoriesChildrenCubit extends Cubit { + CategoriesChildrenCubit() : super(CategoriesChildrenInitial()); + + void updateCategories(List newCategories) { + emit(state.copyWith(categories: newCategories)); + } + + Future getAllChildCategories(String id) async { + if (state.isLoading || state.currentPage > state.totalPages) return; + if (state.currentPage == 1) { + emit(CategoriesChildrenLoading()); + } else { + emit(state.copyWith(isLoading: true)); + } + try { + final response = await CategoryRepository.fetchAllChild( + page: state.currentPage, id: id); + emit(CategoriesChildrenLoaded( + categories: List.from(state.categories)..addAll(response.data ?? []), + isLoading: false, + currentPage: state.currentPage + 1, + totalPages: response.meta?.totalPages ?? 1, + )); + } catch (e) { + emit(CategoriesChildrenError(e.toString())); + } + } + + Future resetPagination() async { + emit(CategoriesChildrenInitial()); + } +} diff --git a/lib/presentation/providers/category/cubit/categories_children_state.dart b/lib/presentation/providers/category/cubit/categories_children_state.dart new file mode 100644 index 0000000..f47ea5f --- /dev/null +++ b/lib/presentation/providers/category/cubit/categories_children_state.dart @@ -0,0 +1,60 @@ +part of 'categories_children_cubit.dart'; + +class CategoriesChildrenState { + final List categories; + final bool isLoading; + final String? errorMessage; + final int currentPage; + final int totalPages; + + const CategoriesChildrenState({ + this.categories = const [], + this.isLoading = false, + this.errorMessage, + this.currentPage = 1, + this.totalPages = 1, + }); + + CategoriesChildrenState copyWith({ + List? categories, + bool? isLoading, + String? errorMessage, + int? currentPage, + int? totalPages, + }) { + return CategoriesChildrenState( + categories: categories ?? this.categories, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage ?? this.errorMessage, + currentPage: currentPage ?? this.currentPage, + totalPages: totalPages ?? this.totalPages, + ); + } +} + +final class CategoriesChildrenInitial extends CategoriesChildrenState { + const CategoriesChildrenInitial() + : super( + categories: const [], + currentPage: 1, + errorMessage: null, + isLoading: false, + totalPages: 1); +} + +final class CategoriesChildrenLoading extends CategoriesChildrenState {} + +final class CategoriesChildrenLoaded extends CategoriesChildrenState { + CategoriesChildrenLoaded({ + super.categories, + super.isLoading, + super.errorMessage, + super.currentPage, + super.totalPages, + }); +} + +final class CategoriesChildrenError extends CategoriesChildrenState { + const CategoriesChildrenError(String errorMessage) + : super(errorMessage: errorMessage); +} diff --git a/lib/presentation/providers/category/cubit/categories_cubit.dart b/lib/presentation/providers/category/cubit/categories_cubit.dart new file mode 100644 index 0000000..7a2a472 --- /dev/null +++ b/lib/presentation/providers/category/cubit/categories_cubit.dart @@ -0,0 +1,38 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/data/models/categories/categories_model.dart'; +import 'package:proxibuy/data/repositories/category_repository.dart'; + +part 'categories_state.dart'; + +class CategoriesCubit extends Cubit { + CategoriesCubit() : super(CategoriesInitial()); + + void updateCategories(List newCategories) { + emit(state.copyWith(categories: newCategories)); + } + + Future getAllCategories() async { + if (state.isLoading || state.currentPage > state.totalPages) return; + if (state.currentPage == 1) { + emit(CategoriesLoading()); + } else { + emit(state.copyWith(isLoading: true)); + } + try { + final response = + await CategoryRepository.fetchAll(page: state.currentPage); + emit(CategoriesLoaded( + categories: List.from(state.categories)..addAll(response.data ?? []), + isLoading: false, + currentPage: state.currentPage + 1, + totalPages: response.meta?.totalPages ?? 1, + )); + } catch (e) { + emit(CategoriesError(e.toString())); + } + } + + Future resetPagination() async { + emit(CategoriesInitial()); + } +} diff --git a/lib/presentation/providers/cubit/categories_state.dart b/lib/presentation/providers/category/cubit/categories_state.dart similarity index 57% rename from lib/presentation/providers/cubit/categories_state.dart rename to lib/presentation/providers/category/cubit/categories_state.dart index 9499310..39f44ee 100644 --- a/lib/presentation/providers/cubit/categories_state.dart +++ b/lib/presentation/providers/category/cubit/categories_state.dart @@ -4,33 +4,54 @@ class CategoriesState { final List categories; final bool isLoading; final String? errorMessage; + final int currentPage; + final int totalPages; const CategoriesState({ this.categories = const [], this.isLoading = false, this.errorMessage, + this.currentPage = 1, + this.totalPages = 1, }); CategoriesState copyWith({ List? categories, bool? isLoading, String? errorMessage, + int? currentPage, + int? totalPages, }) { return CategoriesState( categories: categories ?? this.categories, isLoading: isLoading ?? this.isLoading, errorMessage: errorMessage ?? this.errorMessage, + currentPage: currentPage ?? this.currentPage, + totalPages: totalPages ?? this.totalPages, ); } } -final class CategoriesInitial extends CategoriesState {} +final class CategoriesInitial extends CategoriesState { + const CategoriesInitial() + : super( + categories: const [], + currentPage: 1, + errorMessage: null, + isLoading: false, + totalPages: 1); +} final class CategoriesLoading extends CategoriesState {} final class CategoriesLoaded extends CategoriesState { - const CategoriesLoaded(List categories) - : super(categories: categories); + CategoriesLoaded({ + super.categories, + super.isLoading, + super.errorMessage, + super.currentPage, + super.totalPages, + }); } final class CategoriesError extends CategoriesState { diff --git a/lib/presentation/providers/category/cubit/category_cubit.dart b/lib/presentation/providers/category/cubit/category_cubit.dart new file mode 100644 index 0000000..7d74ed1 --- /dev/null +++ b/lib/presentation/providers/category/cubit/category_cubit.dart @@ -0,0 +1,19 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/data/models/categories/categories_model.dart'; +import 'package:proxibuy/data/repositories/category_repository.dart'; + +part 'category_state.dart'; + +class CategoryCubit extends Cubit { + CategoryCubit() : super(CategoryInitial()); + + void getCategory(String id) async { + emit(CategoryLoading()); + try { + final response = await CategoryRepository.fetchOne(id: id); + emit(CategorySuccess(category: response)); + } catch (e) { + emit(CategoryFail(message: e.toString())); + } + } +} diff --git a/lib/presentation/providers/category/cubit/category_state.dart b/lib/presentation/providers/category/cubit/category_state.dart new file mode 100644 index 0000000..6f37071 --- /dev/null +++ b/lib/presentation/providers/category/cubit/category_state.dart @@ -0,0 +1,19 @@ +part of 'category_cubit.dart'; + +sealed class CategoryState {} + +final class CategoryInitial extends CategoryState {} + +final class CategorySuccess extends CategoryState { + final CategoriesModel category; + + CategorySuccess({required this.category}); +} + +final class CategoryFail extends CategoryState { + final String? message; + + CategoryFail({required this.message}); +} + +final class CategoryLoading extends CategoryState {} diff --git a/lib/presentation/providers/cubit/categories_cubit.dart b/lib/presentation/providers/cubit/categories_cubit.dart deleted file mode 100644 index bda88b8..0000000 --- a/lib/presentation/providers/cubit/categories_cubit.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:proxibuy/data/models/categories/categories_model.dart'; -import 'package:proxibuy/data/repositories/category_repository.dart'; - -part 'categories_state.dart'; - -class CategoriesCubit extends Cubit { - CategoriesCubit() : super(CategoriesInitial()); - - void updateCategories(List newCategories) { - emit(state.copyWith(categories: newCategories)); - } - - Future getAllCategories() async { - emit(state.copyWith(isLoading: true)); - try { - final categories = await CategoryRepository.fetchAll(); - if (categories != null) { - emit(CategoriesLoaded(categories)); - } else { - emit(const CategoriesError('Failed to load categories')); - } - } catch (e) { - emit(CategoriesError(e.toString())); - } - } -} diff --git a/lib/presentation/providers/cubit/them_mode_cubit.dart b/lib/presentation/providers/them_mode_cubit.dart similarity index 100% rename from lib/presentation/providers/cubit/them_mode_cubit.dart rename to lib/presentation/providers/them_mode_cubit.dart diff --git a/lib/presentation/providers/cubit/user_info_cubit.dart b/lib/presentation/providers/user_info_cubit.dart similarity index 100% rename from lib/presentation/providers/cubit/user_info_cubit.dart rename to lib/presentation/providers/user_info_cubit.dart diff --git a/lib/presentation/providers/cubit/user_info_state.dart b/lib/presentation/providers/user_info_state.dart similarity index 100% rename from lib/presentation/providers/cubit/user_info_state.dart rename to lib/presentation/providers/user_info_state.dart diff --git a/lib/presentation/ui/screens/auth/auth_page.dart b/lib/presentation/ui/screens/auth/auth_page.dart index a50ee27..1f3ecfb 100644 --- a/lib/presentation/ui/screens/auth/auth_page.dart +++ b/lib/presentation/ui/screens/auth/auth_page.dart @@ -5,7 +5,7 @@ import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/utils/empty_space.dart'; import 'package:proxibuy/data/models/onboarding_banner_model.dart'; -import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/providers/them_mode_cubit.dart'; import 'package:proxibuy/presentation/ui/theme/responsive.dart'; import 'package:proxibuy/presentation/ui/widgets/carousel/carousel_slider_widget.dart'; import 'package:proxibuy/presentation/ui/widgets/edit_text/phone_number_input.dart'; diff --git a/lib/presentation/ui/screens/categories/categories_page.dart b/lib/presentation/ui/screens/categories/categories_page.dart new file mode 100644 index 0000000..9b0f5a1 --- /dev/null +++ b/lib/presentation/ui/screens/categories/categories_page.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_children_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/category_cubit.dart'; +import 'package:proxibuy/presentation/ui/widgets/default_placeholder.dart'; + +class CategoriesPage extends StatefulWidget { + final String id; + const CategoriesPage({super.key, required this.id}); + + @override + State createState() => _CategoriesPageState(); +} + +class _CategoriesPageState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await context.read().resetPagination(); + if (mounted) { + context + .read() + .getAllChildCategories(widget.id); + } + }); + } + + void _onScroll() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + context.read().getAllChildCategories(widget.id); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: BlocBuilder( + builder: (context, state) { + if (state is CategorySuccess) { + return Text(state.category.name ?? '...'); + } + return SizedBox(); + }, + )), + body: BlocBuilder( + builder: (context, state) { + if (state is CategoriesChildrenLoaded) { + if (state.categories.isEmpty) { + return Center(child: Text('Empty')); + } + return SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + // controller: _scrollController, + itemCount: state.categories.length, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + if (index >= state.categories.length) { + return Center(child: CircularProgressIndicator()); + } + final category = state.categories[index]; + return ListTile( + leading: SvgPicture.network( + '${state.categories[index].url}', + color: Theme.of(context).colorScheme.onSurface, + placeholderBuilder: (context) => DefaultPlaceHolder( + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, color: Colors.white), + )), + errorBuilder: (context, error, stackTrace) => + Icon(Icons.category_outlined), + ), + title: Text(category.name ?? ''), + ); + }, + ), + if (state.isLoading) LinearProgressIndicator() + ], + ), + ); + } else if (state is CategoriesChildrenError) { + return Center(child: Text(state.errorMessage!)); + } else { + return ListView.builder( + // controller: _scrollController, + itemCount: 20, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + return DefaultPlaceHolder( + child: Container( + margin: EdgeInsets.symmetric(vertical: 8), + color: Colors.white, + child: ListTile( + leading: Icon(Icons.abc_outlined), + title: Text(''), + ), + ), + ); + }, + ); + } + }, + ), + ); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/ui/screens/home/home_page.dart b/lib/presentation/ui/screens/home/home_page.dart index 7d4985c..ea7f2e5 100644 --- a/lib/presentation/ui/screens/home/home_page.dart +++ b/lib/presentation/ui/screens/home/home_page.dart @@ -6,9 +6,10 @@ import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/utils/empty_space.dart'; import 'package:proxibuy/data/models/screen_model.dart'; -import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/providers/them_mode_cubit.dart'; import 'package:proxibuy/presentation/ui/theme/responsive.dart'; import 'package:proxibuy/presentation/ui/theme/theme.dart'; +import 'package:proxibuy/presentation/ui/widgets/navigations/categories_mega_menu.dart'; class HomePage extends StatefulWidget { final Widget child; @@ -55,65 +56,70 @@ class _HomePageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Row( + children: [ + SelectableText( + "Proxibuy", + style: Theme.of(context).textTheme.displaySmall, + ), + 32.w, + Row( + children: [ + ...List.generate( + screens.length, + (index) => Row( + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: selectedIndex == index + ? themeColor(context) + ?.primaryLightSurface + .withAlpha(90) + : null), + child: IconButton( + splashRadius: 12, + onPressed: () { + _onItemTapped(context, index); + }, + icon: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + screens[index].icon.svg( + color: selectedIndex == index + ? Theme.of(context).primaryColor + : Theme.of(context) + .colorScheme + .onSurface), + 12.w, + Text( + screens[index].title, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: selectedIndex == index + ? Theme.of(context).primaryColor + : Theme.of(context) + .colorScheme + .onSurface), + ) + ], + )), + ), + 24.w + ], + ), + ), + CategoriesMegaMenu() + ], + ), + ], + ), Flexible( child: Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - SelectableText( - "Proxibuy", - style: Theme.of(context).textTheme.displaySmall, - ), - 32.w, - Row( - children: [ - ...List.generate( - screens.length, - (index) => Row( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: selectedIndex == index - ? themeColor(context) - ?.primaryLightSurface - .withAlpha(90) - : null), - child: IconButton( - splashRadius: 12, - onPressed: () { - _onItemTapped(context, index); - }, - icon: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - screens[index].icon.svg( - color: selectedIndex == index - ? Theme.of(context).primaryColor - : Theme.of(context) - .colorScheme - .onSurface), - 12.w, - Text( - screens[index].title, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: selectedIndex == index - ? Theme.of(context) - .primaryColor - : Theme.of(context) - .colorScheme - .onSurface), - ) - ], - )), - ), - 24.w - ], - ), - ) - ], - ), 8.w, Flexible( child: Container( @@ -133,24 +139,20 @@ class _HomePageState extends State { ), )), 32.w, + IconButton( + icon: Assets.icon.outline.notificationBing + .svg(color: Theme.of(context).colorScheme.onSurface), + onPressed: () {}, + ), + 12.w, + IconButton( + icon: Icon(Icons.brightness_6), + onPressed: () { + context.read().changeTheme(); + }, + ), ], ), - ), - Row( - children: [ - IconButton( - icon: Assets.icon.outline.notificationBing - .svg(color: Theme.of(context).colorScheme.onSurface), - onPressed: () {}, - ), - 12.w, - IconButton( - icon: Icon(Icons.brightness_6), - onPressed: () { - context.read().changeTheme(); - }, - ), - ], ) ], ), diff --git a/lib/presentation/ui/screens/home/home_screen.dart b/lib/presentation/ui/screens/home/home_screen.dart index a28c9d1..4f63e23 100644 --- a/lib/presentation/ui/screens/home/home_screen.dart +++ b/lib/presentation/ui/screens/home/home_screen.dart @@ -1,15 +1,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/utils/empty_space.dart'; -import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; -import 'package:proxibuy/presentation/providers/cubit/categories_cubit.dart'; +import 'package:proxibuy/presentation/providers/them_mode_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_cubit.dart'; import 'package:proxibuy/presentation/ui/theme/colors.dart'; import 'package:proxibuy/presentation/ui/theme/responsive.dart'; import 'package:proxibuy/presentation/ui/widgets/carousel/carousel_slider_widget.dart'; +import 'package:proxibuy/presentation/ui/widgets/default_placeholder.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -52,48 +54,8 @@ class _HomeScreenState extends State { child: Column( children: [ 12.h, - titleDivider(context, title: 'what\'s on your mind?', top: 16), - BlocBuilder( - builder: (context, state) { - if (state is CategoriesLoaded) { - return SizedBox( - width: double.infinity, - height: 40, - child: ListView.builder( - itemCount: state.categories.length, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - padding: EdgeInsets.symmetric(horizontal: 10), - physics: BouncingScrollPhysics(), - itemBuilder: (context, index) { - return Container( - width: 200, - height: 40, - margin: EdgeInsets.symmetric(horizontal: 6), - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.surface, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.car_crash), - 8.w, - Text(state.categories[index].name ?? '') - ], - ), - ); - }, - ), - ); - } else if (state is CategoriesError) { - return Text('Failed to load categories'); - } else { - return CircularProgressIndicator(); - } - }, - ), + // categories(context), + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -633,9 +595,13 @@ class _HomeScreenState extends State { BlocBuilder( builder: (context, state) { if (state is CategoriesLoaded) { + if (state.categories.isEmpty) { + return Text('Empty'); + } + return SizedBox( width: double.infinity, - height: 64, + height: Responsive(context).isMobile() ? 64 : 40, child: ListView.builder( itemCount: state.categories.length, scrollDirection: Axis.horizontal, @@ -643,17 +609,54 @@ class _HomeScreenState extends State { padding: EdgeInsets.symmetric(horizontal: 10), physics: BouncingScrollPhysics(), itemBuilder: (context, index) { - return Container( - width: 64, - height: 64, - margin: EdgeInsets.symmetric(horizontal: 6), - padding: EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.surface, - ), - child: Center( - child: Icon(Icons.abc), + ValueNotifier isHovered = ValueNotifier(false); + return Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: InkWell( + onTap: () { + context.go( + '${AppRouter.categories}/${state.categories[index].id}'); + }, + onHover: (value) { + isHovered.value = value; + }, + child: ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, hovered, _) { + return Container( + width: + Responsive(context).isMobile() ? 64 : 200, + height: + Responsive(context).isMobile() ? 64 : 40, + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: hovered + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.network( + '${state.categories[index].url}', + color: Theme.of(context) + .colorScheme + .onSurface, + placeholderBuilder: (context) => + CircularProgressIndicator(), + errorBuilder: + (context, error, stackTrace) => + Icon(Icons.error), + ), + if (!Responsive(context).isMobile()) ...[ + 8.w, + Text(state.categories[index].name ?? '') + ] + ], + ), + ); + }), ), ); }, @@ -662,7 +665,30 @@ class _HomeScreenState extends State { } else if (state is CategoriesError) { return Text('Failed to load categories'); } else { - return CircularProgressIndicator(); + return SizedBox( + width: double.infinity, + height: Responsive(context).isMobile() ? 64 : 40, + child: ListView.builder( + itemCount: 10, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + padding: EdgeInsets.symmetric(horizontal: 10), + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return DefaultPlaceHolder( + child: Container( + width: Responsive(context).isMobile() ? 64 : 200, + height: Responsive(context).isMobile() ? 64 : 40, + padding: EdgeInsets.all(8), + margin: EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surface, + ), + ), + ); + }), + ); } }, ), diff --git a/lib/presentation/ui/theme/custom_colors.dart b/lib/presentation/ui/theme/custom_colors.dart index 2627a8e..fd25708 100644 --- a/lib/presentation/ui/theme/custom_colors.dart +++ b/lib/presentation/ui/theme/custom_colors.dart @@ -4,11 +4,15 @@ class CustomColors extends ThemeExtension { final MaterialColor primarySwatch; final MaterialColor secondrySwatch; final Color primaryLightSurface; + final Color baseColor; + final Color highlightColor; CustomColors({ required this.primarySwatch, required this.secondrySwatch, required this.primaryLightSurface, + required this.baseColor, + required this.highlightColor, }); @override @@ -16,23 +20,30 @@ class CustomColors extends ThemeExtension { MaterialColor? primarySwatch, MaterialColor? secondrySwatch, Color? primaryLightSurface, + Color? baseColor, + Color? highlightColor, }) { return CustomColors( primarySwatch: primarySwatch ?? this.primarySwatch, secondrySwatch: secondrySwatch ?? this.secondrySwatch, primaryLightSurface: primaryLightSurface ?? this.primaryLightSurface, + baseColor: baseColor ?? this.baseColor, + highlightColor: highlightColor ?? this.highlightColor, ); } @override - ThemeExtension lerp( - ThemeExtension? other, double t) { - if (other is! CustomColors) return this; + CustomColors lerp(ThemeExtension? other, double t) { + if (other is! CustomColors) { + return this; + } return CustomColors( primarySwatch: other.primarySwatch, secondrySwatch: other.secondrySwatch, primaryLightSurface: Color.lerp(primaryLightSurface, other.primaryLightSurface, t)!, + baseColor: Color.lerp(baseColor, other.baseColor, t)!, + highlightColor: Color.lerp(highlightColor, other.highlightColor, t)!, ); } } diff --git a/lib/presentation/ui/theme/theme.dart b/lib/presentation/ui/theme/theme.dart index b1a7292..8338785 100644 --- a/lib/presentation/ui/theme/theme.dart +++ b/lib/presentation/ui/theme/theme.dart @@ -12,6 +12,8 @@ final ThemeData appTheme = ThemeData( primarySwatch: primarySwatch, secondrySwatch: secondarySwatch, primaryLightSurface: primarySwatch[200]!, + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, ), ], @@ -91,6 +93,8 @@ final ThemeData darkTheme = ThemeData( primarySwatch: darkPrimarySwatch, secondrySwatch: darkSecondarySwatch, primaryLightSurface: darkPrimarySwatch[600]!, + baseColor: Colors.grey[700]!, + highlightColor: Colors.grey[500]!, ), ], diff --git a/lib/presentation/ui/widgets/default_placeholder.dart b/lib/presentation/ui/widgets/default_placeholder.dart new file mode 100644 index 0000000..92c8c4f --- /dev/null +++ b/lib/presentation/ui/widgets/default_placeholder.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:proxibuy/presentation/ui/theme/theme.dart'; + +class DefaultPlaceHolder extends StatelessWidget { + final Widget child; + final bool enabled; + final double? width; + final double? height; + const DefaultPlaceHolder( + {super.key, + required this.child, + this.enabled = true, + this.width, + this.height}); + + @override + Widget build(BuildContext context) { + final colors = themeColor(context); + return enabled + ? IgnorePointer( + ignoring: true, + child: SizedBox( + width: width, + height: height, + child: Shimmer.fromColors( + baseColor: colors?.baseColor ?? Colors.grey[300]!, + highlightColor: colors?.highlightColor ?? Colors.grey[100]!, + child: child), + ), + ) + : child; + } +} diff --git a/lib/presentation/ui/widgets/dialog/image_gallary.dart b/lib/presentation/ui/widgets/dialog/image_gallary.dart index c6b560c..854451f 100644 --- a/lib/presentation/ui/widgets/dialog/image_gallary.dart +++ b/lib/presentation/ui/widgets/dialog/image_gallary.dart @@ -46,18 +46,6 @@ class ImageGallary extends ModalRoute { late final ValueNotifier initialImage = ValueNotifier(initialUrl); Widget _buildOverlayContent(BuildContext context) { - double x = 1; - if (MediaQuery.sizeOf(context).width < 400) { - x = 0.27; - } else if (MediaQuery.sizeOf(context).width < 600) { - x = 0.2; - } else if (Responsive(context).isMobile()) { - x = 0.5; - } else if (Responsive(context).isTablet()) { - x = 1; - } else { - x = 1.5; - } double viewportFraction = (1 / (urls.length * (MediaQuery.sizeOf(context).width / 1300))); diff --git a/lib/presentation/ui/widgets/hover_submenu.dart b/lib/presentation/ui/widgets/hover_submenu.dart new file mode 100644 index 0000000..3c3d8c0 --- /dev/null +++ b/lib/presentation/ui/widgets/hover_submenu.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +class HoverSubmenu extends StatefulWidget { + final String title; + final List submenuItems; + + const HoverSubmenu( + {super.key, required this.title, required this.submenuItems}); + + @override + State createState() => _HoverSubmenuState(); +} + +class _HoverSubmenuState extends State { + OverlayEntry? _submenuOverlay; + final LayerLink _submenuLink = LayerLink(); + + void _showSubmenu(BuildContext context) { + _submenuOverlay = _createSubmenu(); + Overlay.of(context).insert(_submenuOverlay!); + } + + void _hideSubmenu() { + _submenuOverlay?.remove(); + _submenuOverlay = null; + } + + OverlayEntry _createSubmenu() { + return OverlayEntry( + builder: (context) => Positioned( + left: 250, // Adjust submenu position + top: 0, + child: CompositedTransformFollower( + link: _submenuLink, + offset: Offset(200, 0), + child: MouseRegion( + onExit: (_) => _hideSubmenu(), + child: Material( + elevation: 4, + color: Colors.white, + child: Container( + width: 200, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: widget.submenuItems + .map((item) => ListTile( + title: Text(item), + onTap: () { + _hideSubmenu(); + print('$item clicked'); + }, + )) + .toList(), + ), + ), + ), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return CompositedTransformTarget( + link: _submenuLink, + child: MouseRegion( + onEnter: (_) => _showSubmenu(context), + child: ListTile( + title: Text(widget.title), + trailing: Icon(Icons.arrow_right), + onTap: () {}, + ), + ), + ); + } + + @override + void dispose() { + _submenuOverlay?.remove(); + _submenuOverlay = null; + super.dispose(); + } +} diff --git a/lib/presentation/ui/widgets/navigations/categories_mega_menu.dart b/lib/presentation/ui/widgets/navigations/categories_mega_menu.dart new file mode 100644 index 0000000..e51d6f4 --- /dev/null +++ b/lib/presentation/ui/widgets/navigations/categories_mega_menu.dart @@ -0,0 +1,257 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:proxibuy/core/utils/empty_space.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_children_cubit.dart'; +import 'package:proxibuy/presentation/providers/category/cubit/categories_cubit.dart'; +import 'package:proxibuy/presentation/ui/theme/colors.dart'; +import 'package:proxibuy/presentation/ui/theme/theme.dart'; + +class CategoriesMegaMenu extends StatefulWidget { + const CategoriesMegaMenu({super.key}); + + @override + State createState() => _CategoriesMegaMenuState(); +} + +class _CategoriesMegaMenuState extends State { + OverlayEntry? _overlayEntry; + final LayerLink _layerLink = LayerLink(); + ValueNotifier onHovered = ValueNotifier(false); + late ValueNotifier selectedId = ValueNotifier(null); + bool visibleMenu = false; + void _showMegaMenu() { + _overlayEntry = _createMegaMenu(); + Overlay.of(context).insert(_overlayEntry!); + visibleMenu = true; + } + + void _hideMegaMenu() { + _overlayEntry?.remove(); + _overlayEntry = null; + visibleMenu = false; + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + selectedId.value = + context.read().state.categories.first.id; + }); + + context.read().stream.listen( + (state) { + if (!state.isLoading && state.categories.isNotEmpty) { + selectedId.value = state.categories.first.id!; + if (mounted) { + context + .read() + .getAllChildCategories(selectedId.value!); + } + } + }, + ); + + super.initState(); + } + + @override + void dispose() { + _hideMegaMenu(); + super.dispose(); + } + + OverlayEntry _createMegaMenu() { + return OverlayEntry( + builder: (context) => Positioned( + width: 800, // Adjust width of the mega menu + child: CompositedTransformFollower( + link: _layerLink, + offset: Offset(0, 50), // Adjust position below the button + child: MouseRegion( + onExit: (_) => _hideMegaMenu(), + child: Material( + elevation: 4, + child: Container( + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surface, + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)], + ), + child: Row( + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.categories.isEmpty) { + return Center(child: CircularProgressIndicator()); + } else if (state.errorMessage != null) { + return Center(child: Text(state.errorMessage!)); + } else { + return SingleChildScrollView( + child: ValueListenableBuilder( + valueListenable: selectedId, + builder: (context, id, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...List.generate( + context + .read() + .state + .categories + .length, + (index) { + final cat = context + .read() + .state + .categories[index]; + return InkWell( + onTap: () { + if (cat.id != null) { + selectedId.value = cat.id!; + context + .read< + CategoriesChildrenCubit>() + .resetPagination(); + context + .read< + CategoriesChildrenCubit>() + .getAllChildCategories( + id!); + } + }, + child: Container( + color: cat.id == id + ? semanticBlue + : null, + child: _menuItem( + cat.name ?? ''))); + }, + ) + ], + ); + }), + ); + } + }, + ), + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.categories.isEmpty) { + return Center(child: CircularProgressIndicator()); + } else if (state.errorMessage != null) { + return Center(child: Text(state.errorMessage!)); + } else { + return NotificationListener( + onNotification: (scrollNotification) { + if (scrollNotification.metrics.pixels == + scrollNotification + .metrics.maxScrollExtent) { + context + .read() + .getAllChildCategories(selectedId.value!); + } + return false; + }, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 300), // Adjust height as needed + child: ListView.builder( + shrinkWrap: true, + itemCount: state.categories.length + + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= state.categories.length) { + return Center( + child: CircularProgressIndicator()); + } + final category = state.categories[index]; + return ListTile( + title: Text(category.name ?? ''), + onTap: () { + print('${category.name} clicked'); + }, + ); + }, + ), + ), + ); + } + }, + ), + ) + ], + ), + ), + ), + ), + ), + ), + ); + } + + Widget _menuItem(String title) { + return ListTile( + title: Text(title), + ); + } + + @override + Widget build(BuildContext context) { + return CompositedTransformTarget( + link: _layerLink, + child: InkWell( + onTap: () {}, + onHover: (value) { + onHovered.value = value; + }, + child: ValueListenableBuilder( + valueListenable: onHovered, + builder: (context, hovered, _) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: hovered + ? themeColor(context)?.primaryLightSurface.withAlpha(90) + : null), + child: IconButton( + splashRadius: 12, + onPressed: () { + if (visibleMenu) { + _hideMegaMenu(); + } else { + _showMegaMenu(); + } + }, + icon: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Icon(Icons.category, + color: hovered + ? Theme.of(context).primaryColor + : Theme.of(context).colorScheme.onSurface), + 12.w, + Text( + 'Categories', + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: hovered + ? Theme.of(context).primaryColor + : Theme.of(context) + .colorScheme + .onSurface), + ) + ], + )), + ); + }), + ), + ); + } +} diff --git a/lib/presentation/ui/widgets/navigations/drop_down_demo2.dart b/lib/presentation/ui/widgets/navigations/drop_down_demo2.dart new file mode 100644 index 0000000..f2aaac3 --- /dev/null +++ b/lib/presentation/ui/widgets/navigations/drop_down_demo2.dart @@ -0,0 +1,264 @@ +import 'package:flutter/material.dart'; +import 'package:ll_dropdown_menu/ll_dropdown_menu.dart'; + +import 'package:flutter/services.dart'; + +/// Wrapper AppBar for customizing default values +class WrapperAppBar extends AppBar { + static Widget? _defaultLeading; + static Widget? _defaultTitle; + static TextStyle? _defaultToolbarTextStyle; + static TextStyle? _defaultTitleTextStyle; + static double? _defaultToolbarHeight; + static SystemUiOverlayStyle? _defaultSystemOverlayStyle; + + static void initConfig({ + Widget? defaultLeading, + Widget? defaultTitle, + TextStyle? defaultToolbarTextStyle, + TextStyle? defaultTitleTextStyle, + double? defaultToolbarHeight, + SystemUiOverlayStyle? defaultSystemOverlayStyle, + }) { + _defaultLeading = defaultLeading; + _defaultTitle = defaultTitle; + _defaultToolbarTextStyle = defaultToolbarTextStyle; + _defaultTitleTextStyle = defaultTitleTextStyle; + _defaultToolbarHeight = defaultToolbarHeight; + _defaultSystemOverlayStyle = defaultSystemOverlayStyle; + } + + WrapperAppBar({ + super.key, + Widget? leading, + super.automaticallyImplyLeading, + Widget? title, + super.actions, + super.flexibleSpace, + super.bottom, + double super.elevation = 0, + super.scrolledUnderElevation, + super.notificationPredicate, + super.shadowColor, + super.surfaceTintColor, + super.shape, + Color super.backgroundColor = Colors.white, + super.foregroundColor, + super.iconTheme, + super.actionsIconTheme, + super.primary, + bool super.centerTitle = true, + super.excludeHeaderSemantics, + super.titleSpacing, + super.toolbarOpacity, + super.bottomOpacity, + double? toolbarHeight, + super.leadingWidth, + TextStyle? toolbarTextStyle, + TextStyle? titleTextStyle, + SystemUiOverlayStyle? systemOverlayStyle, + super.forceMaterialTransparency, + super.clipBehavior, + String? titleText, + }) : super( + leading: + leading ?? (automaticallyImplyLeading ? _defaultLeading : null), + title: title ?? + _defaultTitle ?? + Text(titleText ?? '', style: titleTextStyle), + toolbarHeight: toolbarHeight ?? _defaultToolbarHeight, + toolbarTextStyle: toolbarTextStyle ?? _defaultToolbarTextStyle, + titleTextStyle: titleTextStyle ?? + _defaultTitleTextStyle ?? + const TextStyle(color: Colors.black, fontSize: 18), + systemOverlayStyle: systemOverlayStyle ?? _defaultSystemOverlayStyle, + ); +} + +class DropDownDemo3 extends StatefulWidget { + const DropDownDemo3({super.key}); + + @override + State createState() => _DropDownDemoState(); +} + +class _DropDownDemoState extends State + with SingleTickerProviderStateMixin { + final DropDownController dropDownController = DropDownController(); + final DropDownCascadeListDataController dataController1 = + DropDownCascadeListDataController(); + final DropDownCascadeListDataController dataController2 = + DropDownCascadeListDataController(); + final GlobalKey scaffoldKey = GlobalKey(); + + List>> data1 = List.generate( + 6, + (index) => DropDownItem>( + text: "Tab $index", + data: List.generate( + index + 2, + (index) => DropDownItem( + text: "Tab Second $index", + activeIcon: const Icon(Icons.check), + ), + ), + ), + ); + List>> data2 = List.generate( + 6, + (index) => DropDownItem>( + text: "Tab $index", + data: List.generate( + index + 2, + (index) => DropDownItem( + text: "Tab Second $index", + activeIcon: const Icon(Icons.check), + ), + ), + ), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: scaffoldKey, + appBar: WrapperAppBar( + titleText: "DropDownDemo Custom", + backgroundColor: Colors.white, + actions: const [ + SizedBox(), + ], + ), + body: Column(children: [ + DropDownHeader( + controller: dropDownController, + boxStyle: const DropDownBoxStyle( + height: 50, + backgroundColor: Colors.white, + margin: EdgeInsets.symmetric(horizontal: 10), + padding: EdgeInsets.symmetric(horizontal: 10), + ), + itemStyle: DropDownItemStyle( + activeIconColor: Colors.blue, + activeTextStyle: const TextStyle(color: Colors.blue), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(20), + ), + activeDecoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(20), + ), + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), + alignment: Alignment.center, + highlightTextStyle: const TextStyle(color: Colors.white), + highlightDecoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(20), + ), + highlightIcon: const Icon( + Icons.arrow_drop_down, + color: Colors.white, + ), + ), + items: List.generate( + 3, + (index) => DropDownItem( + text: index == 2 ? "Filter" : "Tab $index", + icon: index == 2 + ? const Icon(Icons.filter_alt) + : const Icon(Icons.arrow_drop_down), + activeIcon: index == 2 + ? const Icon(Icons.filter_alt) + : const Icon(Icons.arrow_drop_up), + ), + ), + onItemTap: (index, item) { + if (index == 2) { + dropDownController.hide(); + scaffoldKey.currentState?.openEndDrawer(); + } else { + dropDownController.toggle(index); + } + }, + ), + Expanded( + child: Stack( + children: [ + Container( + color: Colors.blue[200], + child: Center( + child: TextButton( + onPressed: () { + dataController1.resetAllItemsStatus(); + dataController2.resetAllItemsStatus(); + for (int i = 0; i < 2; i++) { + dropDownController.hide( + index: i, + status: DropDownHeaderStatus(text: "Tab$i"), + ); + } + }, + child: const Text("Reset"), + ), + ), + ), + DropDownView( + controller: dropDownController, + builders: [ + DropDownCascadeList( + controller: dropDownController, + dataController: dataController1, + headerIndex: 0, + secondFloorItemStyle: const DropDownItemStyle( + backgroundColor: Colors.white, + activeBackgroundColor: Color(0xFFF5F5F5), + activeTextStyle: + TextStyle(fontSize: 14, color: Colors.blue), + activeIconColor: Colors.blue, + padding: EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerLeft, + textExpand: true, + ), + items: data1, + ), + DropDownCascadeList( + controller: dropDownController, + dataController: dataController2, + headerIndex: 1, + secondFloorItemStyle: const DropDownItemStyle( + backgroundColor: Colors.white, + activeBackgroundColor: Color(0xFFF5F5F5), + activeTextStyle: + TextStyle(fontSize: 14, color: Colors.blue), + activeIconColor: Colors.blue, + padding: EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerLeft, + textExpand: true, + ), + items: data2, + maxMultiChoiceSize: 3, + ), + DropDownViewWrapper( + width: MediaQuery.of(context).size.width, + height: 300, + child: Container( + color: Colors.yellow, + height: 300, + ), + ), + ], + ), + ], + ), + ), + ]), + ); + } + + @override + void dispose() { + dropDownController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/ui/widgets/navigations/mega_menu.dart b/lib/presentation/ui/widgets/navigations/mega_menu.dart new file mode 100644 index 0000000..55cec4e --- /dev/null +++ b/lib/presentation/ui/widgets/navigations/mega_menu.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class MyMegaMenu extends StatelessWidget { + const MyMegaMenu({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 300, + height: 120, + child: DropdownButton( + value: 'Menu', + icon: Icon(Icons.av_timer), + iconSize: 24, + elevation: 16, + style: TextStyle(color: Colors.deepPurple), + underline: Container( + height: 2, + color: Colors.deepPurpleAccent, + ), + onChanged: (newValue) {}, + items: ['Menu', 'Home', 'Profile', 'Settings'] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList() + ..add(DropdownMenuItem( + child: ExpansionTile( + title: Text('Menu'), + children: [ + ListTile( + title: Text('Submenu 1'), + ), + ListTile( + title: Text('Submenu 2'), + ), + ], + ), + ))), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4d84498..59bb2d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -544,6 +544,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.1" + ll_dropdown_menu: + dependency: "direct main" + description: + name: ll_dropdown_menu + sha256: ae752c626b8207a86479013efce9b7a3181ac9eb864756f1071934bec2d954db + url: "https://pub.dev" + source: hosted + version: "0.8.0" logging: dependency: transitive description: @@ -832,6 +840,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + 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 ad420f9..bd94044 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,8 @@ dependencies: pretty_dio_logger: ^1.4.0 universal_html: ^2.2.4 dropdown_textfield: ^1.2.0 + ll_dropdown_menu: ^0.8.0 + shimmer: ^3.0.0 flutter_launcher_icons: android: true