categories handle

This commit is contained in:
OkaykOrhmn 2025-02-19 17:12:49 +03:30
parent 5b993e5c4c
commit 5144f1c446
32 changed files with 1304 additions and 195 deletions

View File

@ -10,6 +10,7 @@
analyzer: analyzer:
errors: errors:
avoid_print: ignore avoid_print: ignore
deprecated_member_use: ignore
deprecated_member_use_from_same_package: ignore deprecated_member_use_from_same_package: ignore
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml

View File

@ -0,0 +1 @@
// This file is no longer needed and can be deleted.

View File

@ -1,19 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.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/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/explore_screen.dart';
import 'package:proxibuy/presentation/ui/screens/home/home_page.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/home_screen.dart';
import 'package:proxibuy/presentation/ui/screens/home/setting_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 { class AppRouter {
static final initial = '/'; static final initial = '/';
static final explore = '/explore'; static final explore = '/explore';
static final setting = '/settings'; static final setting = '/settings';
static final product = '/product'; static final product = '/product';
static final categories = '/categories';
static List<String> home = [initial, explore, setting]; static List<String> home = [initial, explore, setting];
@ -79,9 +82,20 @@ class AppRouter {
GoRoute( GoRoute(
path: product, path: product,
builder: (BuildContext context, GoRouterState state) { 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<CategoryCubit>(
create: (context) => CategoryCubit()..getCategory(id),
child: CategoriesPage(
id: id,
),
);
}, },
routes: <RouteBase>[],
), ),
]), ]),
]), ]),

View File

@ -10,8 +10,7 @@ class ApiService {
late Dio _dio; late Dio _dio;
void setAuthToken(String token) { void setAuthToken(String token) {
_dio.options.headers['Authorization'] = _dio.options.headers['Authorization'] = 'Bearer $token';
'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJheC1zX3N3Tm5fU1hDTVkzWFowSDVKekNhQ0psVXh6bmZ0WHBxSk1YUEF3In0.eyJleHAiOjE3Mzk4ODcwNzksImlhdCI6MTczOTg4NTI3OSwianRpIjoiYTlmZGE4NzItZmJhZC00ZmQ5LTg3MTMtMTcwYjM3MWE1NTM2IiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5saWFyYS5ydW4vcmVhbG1zL2xiYSIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJmMzljODIxNi0zODhhLTQ0ZTEtODVhOC00Zjk5NmU2NmU2MDQiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJmcm9udGVuZCIsInNpZCI6IjIwNWNmNTBkLTE5MWUtNGViMS1iODBkLTMzMTFiNjIzYTZhMiIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cHM6Ly9sYmEtYXBpLmxpYXJhLnJ1bi8qIiwiaHR0cDovL2xvY2FsaG9zdDozMDAwLyoiLCIvKiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1sYmEiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19LCJmcm9udGVuZCI6eyJyb2xlcyI6WyJzaG9wIiwidXNlciJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwibmFtZSI6ImRlbW8gZGVtbyIsInByZWZlcnJlZF91c2VybmFtZSI6ImRlbW8iLCJnaXZlbl9uYW1lIjoiZGVtbyIsImZhbWlseV9uYW1lIjoiZGVtbyIsImVtYWlsIjoiZGVtb0BnbWFpbC5jb20ifQ.r_RtuJLZ9HQZdoy1Fi29d7hbXqcQ497XThKUmAjd_ClenE2VjpTzpogTOFwrrzJqRxm9fDpBflliqBWRg7KwR7irxW4HHBBaJmCaLzLbG8EtiuvciwDc9ugqLwUvGs2Gnzc7P0RA9aWzVY2lBkDZa7GeEvCCI4k0pbSWsAjX-Ax2vHxCqW8GzMsBeLkQ2BE9cyKX5Q3f-yne5HjDoxF1350qPlXwxIRkmM2Ct-aiMm7CjCaawPdTqdritsWejwTtaVCQtzkHPyeToPwE_X1YLWDlFdwpWSzjiI2fDFyV-MykLgDZevxHtvFoIg-2f6Zsm2_t7AMoCr-tM7vquCZTaw';
} }
ApiService() { ApiService() {
@ -28,7 +27,8 @@ class ApiService {
)..interceptors.add(PrettyDioLogger( )..interceptors.add(PrettyDioLogger(
enabled: kDebugMode, 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 /// 🔹 Handle GET requests

View File

@ -47,10 +47,9 @@ class Meta {
currentPage = json['currentPage']; currentPage = json['currentPage'];
totalPages = json['totalPages']; totalPages = json['totalPages'];
if (json['sortBy'] != null) { if (json['sortBy'] != null) {
// sortBy = <List<String>>[]; sortBy = (json['sortBy'] as List)
// json['sortBy'].forEach((v) { .map((item) => (item as List).map((e) => e.toString()).toList())
// sortBy!.addAll(v.toList()); .toList();
// });
} }
} }

View File

@ -1,24 +1,56 @@
class CategoriesModel { class CategoriesModel {
String? id; String? id;
String? createdAt;
String? updatedAt;
String? state;
String? name; String? name;
int? count; int? count;
String? description;
String? createdBy;
String? parentId; 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<String, dynamic> json) { CategoriesModel.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];
createdAt = json['createdAt'];
updatedAt = json['updatedAt'];
state = json['state'];
name = json['name']; name = json['name'];
count = json['count']; count = json['count'];
description = json['description'];
createdBy = json['created_by'];
parentId = json['parent_id']; parentId = json['parent_id'];
icon = json['icon'];
url = json['url'];
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['createdAt'] = createdAt;
data['updatedAt'] = updatedAt;
data['state'] = state;
data['name'] = name; data['name'] = name;
data['count'] = count; data['count'] = count;
data['description'] = description;
data['created_by'] = createdBy;
data['parent_id'] = parentId; data['parent_id'] = parentId;
data['icon'] = icon;
data['url'] = url;
return data; return data;
} }
} }

View File

@ -4,10 +4,10 @@ import 'package:proxibuy/core/services/api/response_model.dart';
import 'package:proxibuy/data/models/categories/categories_model.dart'; import 'package:proxibuy/data/models/categories/categories_model.dart';
class CategoryRepository { class CategoryRepository {
static Future<List<CategoriesModel>?> fetchAll() async { static Future<ResponseModel<List<CategoriesModel>>> fetchAll(
{int page = 1}) async {
try { try {
var response = await apiService.get(ApiRoutes.category); var response = await apiService.get('${ApiRoutes.category}?page=$page');
print("Users: $response");
final res = ResponseModel<List<CategoriesModel>>.fromJson( final res = ResponseModel<List<CategoriesModel>>.fromJson(
response, response,
(data) { (data) {
@ -16,7 +16,37 @@ class CategoryRepository {
.toList(); .toList();
}, },
); );
return res.data; return res;
} catch (e) {
print("Error: $e");
rethrow;
}
}
static Future<ResponseModel<List<CategoriesModel>>> 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<List<CategoriesModel>>.fromJson(
response,
(data) {
return (data as List)
.map((item) => CategoriesModel.fromJson(item))
.toList();
},
);
return res;
} catch (e) {
print("Error: $e");
rethrow;
}
}
static Future<CategoriesModel> fetchOne({required final String id}) async {
try {
var response = await apiService.get('${ApiRoutes.category}/$id');
return CategoriesModel.fromJson(response['category']);
} catch (e) { } catch (e) {
print("Error: $e"); print("Error: $e");
rethrow; rethrow;

View File

@ -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/routes/app_router.dart';
import 'package:proxibuy/core/utils/my_custom_scroll_behavior.dart'; import 'package:proxibuy/core/utils/my_custom_scroll_behavior.dart';
import 'package:proxibuy/data/storage/shared_preferences_helper.dart'; import 'package:proxibuy/data/storage/shared_preferences_helper.dart';
import 'package:proxibuy/presentation/providers/cubit/categories_cubit.dart'; import 'package:proxibuy/presentation/providers/category/cubit/categories_children_cubit.dart';
import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; import 'package:proxibuy/presentation/providers/category/cubit/categories_cubit.dart';
import 'package:proxibuy/presentation/providers/cubit/user_info_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:proxibuy/presentation/ui/theme/theme.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
@ -19,6 +20,8 @@ void main() async {
runApp(MultiBlocProvider( runApp(MultiBlocProvider(
providers: [ providers: [
BlocProvider<ThemModeCubit>(create: (context) => ThemModeCubit()), BlocProvider<ThemModeCubit>(create: (context) => ThemModeCubit()),
BlocProvider<CategoriesChildrenCubit>(
create: (context) => CategoriesChildrenCubit()),
BlocProvider<CategoriesCubit>(create: (context) => CategoriesCubit()), BlocProvider<CategoriesCubit>(create: (context) => CategoriesCubit()),
BlocProvider<UserInfoCubit>( BlocProvider<UserInfoCubit>(
create: (context) => UserInfoCubit()..getUserInfo()), create: (context) => UserInfoCubit()..getUserInfo()),

View File

@ -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<CategoriesChildrenState> {
CategoriesChildrenCubit() : super(CategoriesChildrenInitial());
void updateCategories(List<CategoriesModel> newCategories) {
emit(state.copyWith(categories: newCategories));
}
Future<void> 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());
}
}

View File

@ -0,0 +1,60 @@
part of 'categories_children_cubit.dart';
class CategoriesChildrenState {
final List<CategoriesModel> 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<CategoriesModel>? 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);
}

View File

@ -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<CategoriesState> {
CategoriesCubit() : super(CategoriesInitial());
void updateCategories(List<CategoriesModel> newCategories) {
emit(state.copyWith(categories: newCategories));
}
Future<void> 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());
}
}

View File

@ -4,33 +4,54 @@ class CategoriesState {
final List<CategoriesModel> categories; final List<CategoriesModel> categories;
final bool isLoading; final bool isLoading;
final String? errorMessage; final String? errorMessage;
final int currentPage;
final int totalPages;
const CategoriesState({ const CategoriesState({
this.categories = const [], this.categories = const [],
this.isLoading = false, this.isLoading = false,
this.errorMessage, this.errorMessage,
this.currentPage = 1,
this.totalPages = 1,
}); });
CategoriesState copyWith({ CategoriesState copyWith({
List<CategoriesModel>? categories, List<CategoriesModel>? categories,
bool? isLoading, bool? isLoading,
String? errorMessage, String? errorMessage,
int? currentPage,
int? totalPages,
}) { }) {
return CategoriesState( return CategoriesState(
categories: categories ?? this.categories, categories: categories ?? this.categories,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage ?? this.errorMessage, 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 CategoriesLoading extends CategoriesState {}
final class CategoriesLoaded extends CategoriesState { final class CategoriesLoaded extends CategoriesState {
const CategoriesLoaded(List<CategoriesModel> categories) CategoriesLoaded({
: super(categories: categories); super.categories,
super.isLoading,
super.errorMessage,
super.currentPage,
super.totalPages,
});
} }
final class CategoriesError extends CategoriesState { final class CategoriesError extends CategoriesState {

View File

@ -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<CategoryState> {
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()));
}
}
}

View File

@ -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 {}

View File

@ -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<CategoriesState> {
CategoriesCubit() : super(CategoriesInitial());
void updateCategories(List<CategoriesModel> newCategories) {
emit(state.copyWith(categories: newCategories));
}
Future<void> 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()));
}
}
}

View File

@ -5,7 +5,7 @@ import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/routes/app_router.dart';
import 'package:proxibuy/core/utils/empty_space.dart'; import 'package:proxibuy/core/utils/empty_space.dart';
import 'package:proxibuy/data/models/onboarding_banner_model.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/theme/responsive.dart';
import 'package:proxibuy/presentation/ui/widgets/carousel/carousel_slider_widget.dart'; import 'package:proxibuy/presentation/ui/widgets/carousel/carousel_slider_widget.dart';
import 'package:proxibuy/presentation/ui/widgets/edit_text/phone_number_input.dart'; import 'package:proxibuy/presentation/ui/widgets/edit_text/phone_number_input.dart';

View File

@ -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<CategoriesPage> createState() => _CategoriesPageState();
}
class _CategoriesPageState extends State<CategoriesPage> {
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
WidgetsBinding.instance.addPostFrameCallback((_) async {
await context.read<CategoriesChildrenCubit>().resetPagination();
if (mounted) {
context
.read<CategoriesChildrenCubit>()
.getAllChildCategories(widget.id);
}
});
}
void _onScroll() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
context.read<CategoriesChildrenCubit>().getAllChildCategories(widget.id);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: BlocBuilder<CategoryCubit, CategoryState>(
builder: (context, state) {
if (state is CategorySuccess) {
return Text(state.category.name ?? '...');
}
return SizedBox();
},
)),
body: BlocBuilder<CategoriesChildrenCubit, CategoriesChildrenState>(
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();
}
}

View File

@ -6,9 +6,10 @@ import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/routes/app_router.dart';
import 'package:proxibuy/core/utils/empty_space.dart'; import 'package:proxibuy/core/utils/empty_space.dart';
import 'package:proxibuy/data/models/screen_model.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/responsive.dart';
import 'package:proxibuy/presentation/ui/theme/theme.dart'; import 'package:proxibuy/presentation/ui/theme/theme.dart';
import 'package:proxibuy/presentation/ui/widgets/navigations/categories_mega_menu.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
final Widget child; final Widget child;
@ -55,65 +56,70 @@ class _HomePageState extends State<HomePage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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( Flexible(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ 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, 8.w,
Flexible( Flexible(
child: Container( child: Container(
@ -133,24 +139,20 @@ class _HomePageState extends State<HomePage> {
), ),
)), )),
32.w, 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<ThemModeCubit>().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<ThemModeCubit>().changeTheme();
},
),
],
) )
], ],
), ),

View File

@ -1,15 +1,17 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:proxibuy/core/gen/assets.gen.dart'; import 'package:proxibuy/core/gen/assets.gen.dart';
import 'package:proxibuy/core/routes/app_router.dart'; import 'package:proxibuy/core/routes/app_router.dart';
import 'package:proxibuy/core/utils/empty_space.dart'; import 'package:proxibuy/core/utils/empty_space.dart';
import 'package:proxibuy/presentation/providers/cubit/them_mode_cubit.dart'; import 'package:proxibuy/presentation/providers/them_mode_cubit.dart';
import 'package:proxibuy/presentation/providers/cubit/categories_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/colors.dart';
import 'package:proxibuy/presentation/ui/theme/responsive.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/carousel/carousel_slider_widget.dart';
import 'package:proxibuy/presentation/ui/widgets/default_placeholder.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -52,48 +54,8 @@ class _HomeScreenState extends State<HomeScreen> {
child: Column( child: Column(
children: [ children: [
12.h, 12.h,
titleDivider(context, title: 'what\'s on your mind?', top: 16), // categories(context),
BlocBuilder<CategoriesCubit, CategoriesState>(
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();
}
},
),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -633,9 +595,13 @@ class _HomeScreenState extends State<HomeScreen> {
BlocBuilder<CategoriesCubit, CategoriesState>( BlocBuilder<CategoriesCubit, CategoriesState>(
builder: (context, state) { builder: (context, state) {
if (state is CategoriesLoaded) { if (state is CategoriesLoaded) {
if (state.categories.isEmpty) {
return Text('Empty');
}
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 64, height: Responsive(context).isMobile() ? 64 : 40,
child: ListView.builder( child: ListView.builder(
itemCount: state.categories.length, itemCount: state.categories.length,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -643,17 +609,54 @@ class _HomeScreenState extends State<HomeScreen> {
padding: EdgeInsets.symmetric(horizontal: 10), padding: EdgeInsets.symmetric(horizontal: 10),
physics: BouncingScrollPhysics(), physics: BouncingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( ValueNotifier<bool> isHovered = ValueNotifier(false);
width: 64, return Padding(
height: 64, padding: EdgeInsets.symmetric(horizontal: 6),
margin: EdgeInsets.symmetric(horizontal: 6), child: InkWell(
padding: EdgeInsets.all(8), onTap: () {
decoration: BoxDecoration( context.go(
borderRadius: BorderRadius.circular(8), '${AppRouter.categories}/${state.categories[index].id}');
color: Theme.of(context).colorScheme.surface, },
), onHover: (value) {
child: Center( isHovered.value = value;
child: Icon(Icons.abc), },
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<HomeScreen> {
} else if (state is CategoriesError) { } else if (state is CategoriesError) {
return Text('Failed to load categories'); return Text('Failed to load categories');
} else { } 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,
),
),
);
}),
);
} }
}, },
), ),

View File

@ -4,11 +4,15 @@ class CustomColors extends ThemeExtension<CustomColors> {
final MaterialColor primarySwatch; final MaterialColor primarySwatch;
final MaterialColor secondrySwatch; final MaterialColor secondrySwatch;
final Color primaryLightSurface; final Color primaryLightSurface;
final Color baseColor;
final Color highlightColor;
CustomColors({ CustomColors({
required this.primarySwatch, required this.primarySwatch,
required this.secondrySwatch, required this.secondrySwatch,
required this.primaryLightSurface, required this.primaryLightSurface,
required this.baseColor,
required this.highlightColor,
}); });
@override @override
@ -16,23 +20,30 @@ class CustomColors extends ThemeExtension<CustomColors> {
MaterialColor? primarySwatch, MaterialColor? primarySwatch,
MaterialColor? secondrySwatch, MaterialColor? secondrySwatch,
Color? primaryLightSurface, Color? primaryLightSurface,
Color? baseColor,
Color? highlightColor,
}) { }) {
return CustomColors( return CustomColors(
primarySwatch: primarySwatch ?? this.primarySwatch, primarySwatch: primarySwatch ?? this.primarySwatch,
secondrySwatch: secondrySwatch ?? this.secondrySwatch, secondrySwatch: secondrySwatch ?? this.secondrySwatch,
primaryLightSurface: primaryLightSurface ?? this.primaryLightSurface, primaryLightSurface: primaryLightSurface ?? this.primaryLightSurface,
baseColor: baseColor ?? this.baseColor,
highlightColor: highlightColor ?? this.highlightColor,
); );
} }
@override @override
ThemeExtension<CustomColors> lerp( CustomColors lerp(ThemeExtension<CustomColors>? other, double t) {
ThemeExtension<CustomColors>? other, double t) { if (other is! CustomColors) {
if (other is! CustomColors) return this; return this;
}
return CustomColors( return CustomColors(
primarySwatch: other.primarySwatch, primarySwatch: other.primarySwatch,
secondrySwatch: other.secondrySwatch, secondrySwatch: other.secondrySwatch,
primaryLightSurface: primaryLightSurface:
Color.lerp(primaryLightSurface, other.primaryLightSurface, t)!, Color.lerp(primaryLightSurface, other.primaryLightSurface, t)!,
baseColor: Color.lerp(baseColor, other.baseColor, t)!,
highlightColor: Color.lerp(highlightColor, other.highlightColor, t)!,
); );
} }
} }

View File

@ -12,6 +12,8 @@ final ThemeData appTheme = ThemeData(
primarySwatch: primarySwatch, primarySwatch: primarySwatch,
secondrySwatch: secondarySwatch, secondrySwatch: secondarySwatch,
primaryLightSurface: primarySwatch[200]!, primaryLightSurface: primarySwatch[200]!,
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
), ),
], ],
@ -91,6 +93,8 @@ final ThemeData darkTheme = ThemeData(
primarySwatch: darkPrimarySwatch, primarySwatch: darkPrimarySwatch,
secondrySwatch: darkSecondarySwatch, secondrySwatch: darkSecondarySwatch,
primaryLightSurface: darkPrimarySwatch[600]!, primaryLightSurface: darkPrimarySwatch[600]!,
baseColor: Colors.grey[700]!,
highlightColor: Colors.grey[500]!,
), ),
], ],

View File

@ -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;
}
}

View File

@ -46,18 +46,6 @@ class ImageGallary extends ModalRoute<void> {
late final ValueNotifier<String> initialImage = ValueNotifier(initialUrl); late final ValueNotifier<String> initialImage = ValueNotifier(initialUrl);
Widget _buildOverlayContent(BuildContext context) { 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 = double viewportFraction =
(1 / (urls.length * (MediaQuery.sizeOf(context).width / 1300))); (1 / (urls.length * (MediaQuery.sizeOf(context).width / 1300)));

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
class HoverSubmenu extends StatefulWidget {
final String title;
final List<String> submenuItems;
const HoverSubmenu(
{super.key, required this.title, required this.submenuItems});
@override
State<HoverSubmenu> createState() => _HoverSubmenuState();
}
class _HoverSubmenuState extends State<HoverSubmenu> {
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();
}
}

View File

@ -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<CategoriesMegaMenu> createState() => _CategoriesMegaMenuState();
}
class _CategoriesMegaMenuState extends State<CategoriesMegaMenu> {
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
ValueNotifier<bool> onHovered = ValueNotifier(false);
late ValueNotifier<String?> 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<CategoriesCubit>().state.categories.first.id;
});
context.read<CategoriesCubit>().stream.listen(
(state) {
if (!state.isLoading && state.categories.isNotEmpty) {
selectedId.value = state.categories.first.id!;
if (mounted) {
context
.read<CategoriesChildrenCubit>()
.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<CategoriesCubit, CategoriesState>(
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<CategoriesCubit>()
.state
.categories
.length,
(index) {
final cat = context
.read<CategoriesCubit>()
.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<CategoriesChildrenCubit,
CategoriesChildrenState>(
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<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification.metrics.pixels ==
scrollNotification
.metrics.maxScrollExtent) {
context
.read<CategoriesChildrenCubit>()
.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),
)
],
)),
);
}),
),
);
}
}

View File

@ -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<DropDownDemo3> createState() => _DropDownDemoState();
}
class _DropDownDemoState extends State<DropDownDemo3>
with SingleTickerProviderStateMixin {
final DropDownController dropDownController = DropDownController();
final DropDownCascadeListDataController dataController1 =
DropDownCascadeListDataController();
final DropDownCascadeListDataController dataController2 =
DropDownCascadeListDataController();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
List<DropDownItem<List<DropDownItem>>> data1 = List.generate(
6,
(index) => DropDownItem<List<DropDownItem>>(
text: "Tab $index",
data: List.generate(
index + 2,
(index) => DropDownItem(
text: "Tab Second $index",
activeIcon: const Icon(Icons.check),
),
),
),
);
List<DropDownItem<List<DropDownItem>>> data2 = List.generate(
6,
(index) => DropDownItem<List<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 <Widget>[
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<Tab>(
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();
}
}

View File

@ -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<String>(
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: <String>['Menu', 'Home', 'Profile', 'Settings']
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList()
..add(DropdownMenuItem(
child: ExpansionTile(
title: Text('Menu'),
children: [
ListTile(
title: Text('Submenu 1'),
),
ListTile(
title: Text('Submenu 2'),
),
],
),
))),
);
}
}

View File

@ -544,6 +544,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" 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: logging:
dependency: transitive dependency: transitive
description: description:
@ -832,6 +840,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

@ -50,6 +50,8 @@ dependencies:
pretty_dio_logger: ^1.4.0 pretty_dio_logger: ^1.4.0
universal_html: ^2.2.4 universal_html: ^2.2.4
dropdown_textfield: ^1.2.0 dropdown_textfield: ^1.2.0
ll_dropdown_menu: ^0.8.0
shimmer: ^3.0.0
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true