From e7ce9d8f909aab9841aaa3424804df3432345174 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Thu, 2 Oct 2025 00:05:25 +0300 Subject: [PATCH] fix: refactor --- lib/core/app_router.dart | 482 ++---------------- lib/core/constants/app_constants.dart | 72 +++ lib/core/cubit/base_cubit.dart | 42 ++ lib/core/cubit/state_utils.dart | 28 + lib/core/routing/auth_routes.dart | 37 ++ lib/core/routing/content_routes.dart | 73 +++ lib/core/routing/main_navigation_routes.dart | 94 ++++ lib/core/routing/other_routes.dart | 229 +++++++++ lib/core/routing/route_constants.dart | 84 +++ lib/core/routing/settings_routes.dart | 62 +++ lib/core/utils/validation_utils.dart | 87 ++++ .../blog_post_create_cubit.dart | 55 +- 12 files changed, 862 insertions(+), 483 deletions(-) create mode 100644 lib/core/constants/app_constants.dart create mode 100644 lib/core/cubit/base_cubit.dart create mode 100644 lib/core/cubit/state_utils.dart create mode 100644 lib/core/routing/auth_routes.dart create mode 100644 lib/core/routing/content_routes.dart create mode 100644 lib/core/routing/main_navigation_routes.dart create mode 100644 lib/core/routing/other_routes.dart create mode 100644 lib/core/routing/route_constants.dart create mode 100644 lib/core/routing/settings_routes.dart create mode 100644 lib/core/utils/validation_utils.dart diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index cc028255..f3715464 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -1,455 +1,53 @@ -import 'package:dio/dio.dart'; -import 'package:eatvio_app/core/const.dart'; -import 'package:eatvio_app/core/firebase_api.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; -import 'package:eatvio_app/core/locator_service.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; -import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; -import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; -import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; -import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; -import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; -import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; -import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; -import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; -import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; -import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; -import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; -import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; -import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; -import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; -import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; -import 'package:eatvio_app/features/feed/models/recipes_model.dart'; -import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; -import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; -import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; -import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; -import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; -import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; -import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; -import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; -import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; -import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; -import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; -import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; -import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; -import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; -import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; -import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; -import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; -import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; -import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/core/routing/auth_routes.dart'; +import 'package:eatvio_app/core/routing/content_routes.dart'; +import 'package:eatvio_app/core/routing/main_navigation_routes.dart'; +import 'package:eatvio_app/core/routing/other_routes.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/routing/settings_routes.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:version/version.dart'; -import 'dart:developer'; +/// Главный роутер приложения class AppRouter { static GlobalKey navigatorKey = GlobalKey(); static GoRouter appRouter = GoRouter( - initialLocation: '/update-screen', + initialLocation: RouteConstants.updateScreen, navigatorKey: navigatorKey, - redirect: (context, state) { - final segments = state.uri.pathSegments; - if (segments.length >= 3) { - final type = segments[1]; - final contentIdentifier = segments[2]; - - switch (type) { - case 'recipe': - FlutterNativeSplash.remove(); - return '/about-recipes-screen?slug=$contentIdentifier'; - case 'blog_post': - FlutterNativeSplash.remove(); - return '/blog-post-screen?id=$contentIdentifier'; - case 'plan': - FlutterNativeSplash.remove(); - return '/about-plan-screen?slug=$contentIdentifier'; - case 'diet': - FlutterNativeSplash.remove(); - return '/about-diet-screen?slug=$contentIdentifier'; - } - } - - return null; - }, + redirect: _handleRedirect, routes: [ - GoRoute( - path: '/auth-screen', - name: 'authScreen', - builder: (context, state) => const AuthScreen(), - routes: [ - GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), - GoRoute( - path: 'forget-password-screen', - name: 'forgetPasswordScreen', - builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), - ), - GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), - ], - ), - ShellRoute( - builder: (context, state, child) { - return BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return BottomNavigationBarWidget(profileModel: state.profile, child: child); - } - return const SizedBox.shrink(); - }, - ); - }, - routes: [ - GoRoute( - path: '/my-profile-screen', - name: 'myProfileScreen', - pageBuilder: (context, state) { - return const NoTransitionPage(child: ProfileScreen()); - }, - ), - GoRoute( - path: '/feed-screen', - name: 'feedScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), - redirect: (context, state) async { - try { - await sl().initialize(); - } catch (e, st) { - log(e.toString(), stackTrace: st); - } - return null; - }, - ), - GoRoute( - path: '/diary-screen', - name: 'diaryScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), - ), - GoRoute( - path: ConsultationsScreen.path, - name: ConsultationsScreen.name, - pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), - ), - GoRoute( - path: '/chat-select-screen', - name: 'chatSelectScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), - ), - GoRoute( - path: '/notifications-screen', - name: 'notificationsScreen', - pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), - ), - GoRoute( - path: '/menu-screen', - name: 'menuScreen', - pageBuilder: (context, state) => NoTransitionPage( - child: BlocBuilder( - builder: (BuildContext context, ProfileAvatarState state) { - if (state is ProfileAvatarStateLoaded) { - return MenuScreen(profileModel: state.profile); - } - return const SizedBox.shrink(); - }, - ), - ), - ), - ], - ), - GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()), - GoRoute( - path: '/welcome-screen', - name: 'welcomeScreen', - builder: (context, state) => const WelcomeScreen(), - redirect: (context, state) async { - try { - final sharedPreferences = await SharedPreferences.getInstance(); - final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; - await sl().registerPushToken(); - if (welcomeScreenWasShowed == false) { - return null; - } else if (await TokenStorage.haveTokenInStorage()) { - return '/feed-screen'; - } else { - return '/auth-screen'; - } - } catch (e) { - return '/feed-screen'; - } - }, - ), - GoRoute( - path: '/update-screen', - name: 'updateScreen', - builder: (context, state) => const UpdateScreen(), - redirect: (context, state) async { - try { - final versions = (await sl().get('$baseUrl/api/app_versions')).data; - PackageInfo packageInfo = await PackageInfo.fromPlatform(); - String version = packageInfo.version; - Version currentVersion = Version.parse(version); - Version latestVersion = Version.parse(versions['min_version']); - if (latestVersion > currentVersion) { - return null; - } else { - return '/welcome-screen'; - } - } catch (e) { - return '/welcome-screen'; - } - }, - ), - GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), - GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), - GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), - GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), - GoRoute( - path: CardSettingsScreen.path, - name: CardSettingsScreen.name, - builder: (context, state) { - final chosenPlanId = state.extra as int?; - return CardSettingsScreen(chosenPlanId: chosenPlanId); - }, - ), - GoRoute( - path: '/about-plan-screen', - name: 'aboutPlanScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute(path: '/glossary-screen', name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), - GoRoute( - path: '/about-recipes-screen', - name: 'aboutRecipesScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); - } - - final data = state.extra as Map; - return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: RecipeCreateScreen.path, - name: RecipeCreateScreen.name, - builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), - ), - GoRoute( - path: '/profile-screen', - name: 'profileScreen', - builder: (context, state) => ProfileScreen(author: state.extra as Author), - ), - GoRoute( - path: '/subs-screen/:slug', - name: 'subsScreen', - builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), - ), - GoRoute( - path: '/about-diet-screen', - name: 'aboutDietScreen', - builder: (context, state) { - final slug = state.uri.queryParameters['slug']; - if (slug != null) { - return AboutDietScreen(slug: slug, index: null, navigateToComments: false); - } - final data = state.extra as Map; - return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); - }, - ), - GoRoute( - path: '/blog-post-create-screen', - name: 'blogPostCreateScreen', - builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), - ), - GoRoute( - path: '/blog-post-screen', - name: 'blogPostScreen', - builder: (context, state) { - final id = state.uri.queryParameters['id']; - final parsedId = int.tryParse(id ?? ''); - if (parsedId != null) { - return BlogPostScreen(id: parsedId); - } - - return BlogPostScreen(id: state.extra as int); - }, - ), - GoRoute( - path: '/chat-screen', - name: 'chatScreen', - builder: (context, state) { - final data = state.extra as Map; - final uuid = data['uuid'] as String; - final sendInitialMessage = data['sendInitialMessage'] as bool?; - return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); - }, - ), - GoRoute( - path: '/empty-chat-screen', - name: 'emptyChatScreen', - builder: (context, state) => EmptyChatScreen(author: state.extra as Author), - ), - GoRoute( - path: '/settings-screen', - name: 'settingsScreen', - builder: (context, state) => const SettingsScreen(), - routes: [ - GoRoute( - path: 'general-info-screen', - name: 'generalInfoScreen', - builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'other-info-screen', - name: 'otherInfoScreen', - builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-activity-screen', - name: 'physicalActivityScreen', - builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'physical-form-screen', - name: 'physicalFormScreen', - builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'pressure-screen', - name: 'pressureScreen', - builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-password-screen', - name: 'changePasswordScreen', - builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), - ), - GoRoute( - path: 'change-email-screen', - name: 'changeEmailScreen', - builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), - ), - ], - ), - GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), - GoRoute( - path: '/stepper-become-consultant-screen', - name: 'stepperBecomeConsultantScreen', - builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), - ), - GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), - GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()), - GoRoute( - path: '/photo-view-screen/:index', - name: 'photoViewScreen', - builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), - ), - GoRoute( - path: CreateDietScreen.path, - name: CreateDietScreen.name, - builder: (context, state) { - final diet = state.extra as Diet?; - return CreateDietScreen(diet: diet); - }, - ), - GoRoute( - path: DietPreviewScreen.path, - name: DietPreviewScreen.name, - builder: (context, state) { - final data = state.extra as Diet; - return DietPreviewScreen(diet: data); - }, - ), - GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()), - GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), - GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), - GoRoute( - path: UserDetailsScreen.path, - name: UserDetailsScreen.name, - builder: (context, state) => UserDetailsScreen(slug: state.extra as String), - ), - GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), - GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), - GoRoute( - path: InfoByOrderScreen.path, - name: InfoByOrderScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final successfulPayment = data['successfulPayment'] as bool?; - return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); - }, - ), - GoRoute( - path: PaymentScreen.path, - name: PaymentScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final planId = data['planId'] as int; - final url = data['url'] as String; - return PaymentScreen(url: url, planId: planId); - }, - ), - GoRoute( - path: UserDiaryScreen.path, - name: UserDiaryScreen.name, - builder: (context, state) { - final data = state.extra as Map; - final diaryId = data['diaryId'] as int; - final currentPlanDay = data['currentDiaryDay'] as int; - return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); - }, - ), - GoRoute( - path: '/verify-screen', - name: 'verifyScreen', - builder: (context, state) => VerifyScreen(passKey: state.extra as String), - ), - GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), - GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), - GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()), + ...AuthRoutes.createRoutes(), + ...MainNavigationRoutes.createRoutes(), + ...ContentRoutes.createRoutes(), + ...SettingsRoutes.createRoutes(), + ...OtherRoutes.createRoutes(), ], ); + + /// Обработка редиректов для deep links + static String? _handleRedirect(BuildContext context, GoRouterState state) { + final segments = state.uri.pathSegments; + if (segments.length >= 3) { + final type = segments[1]; + final contentIdentifier = segments[2]; + + switch (type) { + case 'recipe': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutRecipesScreen}?slug=$contentIdentifier'; + case 'blog_post': + FlutterNativeSplash.remove(); + return '${RouteConstants.blogPostScreen}?id=$contentIdentifier'; + case 'plan': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutPlanScreen}?slug=$contentIdentifier'; + case 'diet': + FlutterNativeSplash.remove(); + return '${RouteConstants.aboutDietScreen}?slug=$contentIdentifier'; + } + } + + return null; + } } diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 00000000..f9c945c9 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -0,0 +1,72 @@ +/// Константы приложения +class AppConstants { + // Размеры + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double extraLargePadding = 32.0; + + // Радиусы + static const double defaultBorderRadius = 8.0; + static const double smallBorderRadius = 4.0; + static const double largeBorderRadius = 16.0; + + // Высоты + static const double defaultButtonHeight = 48.0; + static const double smallButtonHeight = 36.0; + static const double largeButtonHeight = 56.0; + + // Ширины + static const double maxContentWidth = 400.0; + static const double minContentWidth = 280.0; + + // Анимации + static const Duration defaultAnimationDuration = Duration(milliseconds: 300); + static const Duration fastAnimationDuration = Duration(milliseconds: 150); + static const Duration slowAnimationDuration = Duration(milliseconds: 500); + + // Таймауты + static const Duration defaultTimeout = Duration(seconds: 30); + static const Duration shortTimeout = Duration(seconds: 10); + static const Duration longTimeout = Duration(minutes: 2); + + // Лимиты + static const int maxImageSize = 5 * 1024 * 1024; // 5MB + static const int maxVideoSize = 100 * 1024 * 1024; // 100MB + static const int maxTextLength = 1000; + static const int maxTitleLength = 100; + static const int maxDescriptionLength = 500; + + // Пагинация + static const int defaultPageSize = 20; + static const int smallPageSize = 10; + static const int largePageSize = 50; + + // Форматы + static const List supportedImageFormats = ['jpg', 'jpeg', 'png', 'webp']; + static const List supportedVideoFormats = ['mp4', 'mov', 'avi']; + + // Регулярные выражения + static const String emailRegex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; + static const String phoneRegex = r'^\+?[1-9]\d{1,14}$'; + static const String urlRegex = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$'; + + // Сообщения + static const String defaultErrorMessage = 'Произошла ошибка. Попробуйте еще раз.'; + static const String networkErrorMessage = 'Проблема с подключением к интернету.'; + static const String timeoutErrorMessage = 'Превышено время ожидания.'; + static const String unknownErrorMessage = 'Неизвестная ошибка.'; + + // Ключи для SharedPreferences + static const String welcomeScreenShownKey = 'welcomeScreenWasShowed'; + static const String userTokenKey = 'userToken'; + static const String userDataKey = 'userData'; + static const String settingsKey = 'settings'; + + // API endpoints + static const String apiVersion = '/api'; + static const String authEndpoint = '/auth'; + static const String usersEndpoint = '/users'; + static const String postsEndpoint = '/posts'; + static const String commentsEndpoint = '/comments'; +} diff --git a/lib/core/cubit/base_cubit.dart b/lib/core/cubit/base_cubit.dart new file mode 100644 index 00000000..3cd6c9d9 --- /dev/null +++ b/lib/core/cubit/base_cubit.dart @@ -0,0 +1,42 @@ +import 'dart:developer'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Базовый класс для всех Cubit'ов с унифицированной обработкой ошибок +abstract class BaseCubit extends Cubit { + BaseCubit(super.initialState); + + /// Унифицированная обработка ошибок + void handleError(Object error, [StackTrace? stackTrace]) { + log('Error in ${runtimeType}: $error', stackTrace: stackTrace); + // Здесь можно добавить отправку ошибок в Sentry или другую систему мониторинга + } + + /// Безопасное выполнение асинхронных операций + Future safeExecute(Future Function() operation) async { + try { + return await operation(); + } catch (error, stackTrace) { + handleError(error, stackTrace); + return null; + } + } + + /// Безопасное выполнение асинхронных операций с обработкой состояния + Future safeExecuteWithState( + Future Function() operation, { + required State loadingState, + required State Function(T result) successState, + required State Function(Object error) errorState, + }) async { + try { + emit(loadingState); + final result = await operation(); + emit(successState(result)); + return result; + } catch (error, stackTrace) { + handleError(error, stackTrace); + emit(errorState(error)); + return null; + } + } +} diff --git a/lib/core/cubit/state_utils.dart b/lib/core/cubit/state_utils.dart new file mode 100644 index 00000000..9c6dfb78 --- /dev/null +++ b/lib/core/cubit/state_utils.dart @@ -0,0 +1,28 @@ +/// Утилиты для работы с состояниями Cubit'ов +class StateUtils { + /// Проверяет, является ли состояние загрузочным + static bool isLoading(dynamic state) { + return state.toString().contains('loading') || state.toString().contains('Loading'); + } + + /// Проверяет, является ли состояние загруженным + static bool isLoaded(dynamic state) { + return state.toString().contains('loaded') || state.toString().contains('Loaded'); + } + + /// Проверяет, является ли состояние ошибочным + static bool isError(dynamic state) { + return state.toString().contains('error') || state.toString().contains('Error'); + } + + /// Получает сообщение об ошибке из состояния + static String? getErrorMessage(dynamic state) { + if (isError(state)) { + // Попытка извлечь сообщение об ошибке из состояния + final stateString = state.toString(); + final errorMatch = RegExp(r'error[:\s]+([^,}]+)').firstMatch(stateString); + return errorMatch?.group(1)?.trim(); + } + return null; + } +} diff --git a/lib/core/routing/auth_routes.dart b/lib/core/routing/auth_routes.dart new file mode 100644 index 00000000..fc4558d1 --- /dev/null +++ b/lib/core/routing/auth_routes.dart @@ -0,0 +1,37 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/forget_password_screen.dart'; +import 'package:eatvio_app/features/auth/presentation/pages/scan_qr_code_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart'; +import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для аутентификации +class AuthRoutes { + /// Создание маршрутов аутентификации + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.authScreen, + name: 'authScreen', + builder: (context, state) => const AuthScreen(), + routes: [ + GoRoute(path: RouteConstants.registrationScreen, name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()), + GoRoute( + path: RouteConstants.forgetPasswordScreen, + name: 'forgetPasswordScreen', + builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool), + ), + GoRoute(path: RouteConstants.scanQRCodeScreen, name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()), + ], + ), + GoRoute( + path: RouteConstants.verifyScreen, + name: 'verifyScreen', + builder: (context, state) => VerifyScreen(passKey: state.extra as String), + ), + GoRoute(path: RouteConstants.verifyEmailScreen, name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()), + ]; + } +} diff --git a/lib/core/routing/content_routes.dart b/lib/core/routing/content_routes.dart new file mode 100644 index 00000000..69d7ebf4 --- /dev/null +++ b/lib/core/routing/content_routes.dart @@ -0,0 +1,73 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.dart'; +import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_post_create_screen.dart'; +import 'package:eatvio_app/features/diets/presentation/pages/about_diet_screen.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для контента +class ContentRoutes { + /// Создание маршрутов контента + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.aboutPlanScreen, + name: 'aboutPlanScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutPlanScreen(slug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutPlanScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutRecipesScreen, + name: 'aboutRecipesScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null); + } + + final data = state.extra as Map; + return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.aboutDietScreen, + name: 'aboutDietScreen', + builder: (context, state) { + final slug = state.uri.queryParameters['slug']; + if (slug != null) { + return AboutDietScreen(slug: slug, index: null, navigateToComments: false); + } + final data = state.extra as Map; + return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool); + }, + ), + GoRoute( + path: RouteConstants.blogPostCreateScreen, + name: 'blogPostCreateScreen', + builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null), + ), + GoRoute( + path: RouteConstants.blogPostScreen, + name: 'blogPostScreen', + builder: (context, state) { + final id = state.uri.queryParameters['id']; + final parsedId = int.tryParse(id ?? ''); + if (parsedId != null) { + return BlogPostScreen(id: parsedId); + } + + return BlogPostScreen(id: state.extra as int); + }, + ), + ]; + } +} diff --git a/lib/core/routing/main_navigation_routes.dart b/lib/core/routing/main_navigation_routes.dart new file mode 100644 index 00000000..818f625c --- /dev/null +++ b/lib/core/routing/main_navigation_routes.dart @@ -0,0 +1,94 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chats_screen.dart'; +import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:developer'; + +/// Маршруты для основной навигации +class MainNavigationRoutes { + /// Создание маршрутов основной навигации + static List createRoutes() { + return [ + ShellRoute( + builder: (context, state, child) { + return BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return BottomNavigationBarWidget(profileModel: state.profile, child: child); + } + return const SizedBox.shrink(); + }, + ); + }, + routes: [ + GoRoute( + path: RouteConstants.myProfileScreen, + name: 'myProfileScreen', + pageBuilder: (context, state) { + return const NoTransitionPage(child: ProfileScreen()); + }, + ), + GoRoute( + path: RouteConstants.feedScreen, + name: 'feedScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()), + redirect: (context, state) async { + try { + await sl().initialize(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return null; + }, + ), + GoRoute( + path: RouteConstants.diaryScreen, + name: 'diaryScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()), + ), + GoRoute( + path: ConsultationsScreen.path, + name: ConsultationsScreen.name, + pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()), + ), + GoRoute( + path: RouteConstants.chatSelectScreen, + name: 'chatSelectScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()), + ), + GoRoute( + path: RouteConstants.notificationsScreen, + name: 'notificationsScreen', + pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()), + ), + GoRoute( + path: RouteConstants.menuScreen, + name: 'menuScreen', + pageBuilder: (context, state) => NoTransitionPage( + child: BlocBuilder( + builder: (BuildContext context, ProfileAvatarState state) { + if (state is ProfileAvatarStateLoaded) { + return MenuScreen(profileModel: state.profile); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ], + ), + ]; + } +} diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart new file mode 100644 index 00000000..43cf885f --- /dev/null +++ b/lib/core/routing/other_routes.dart @@ -0,0 +1,229 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; +import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/empty_chat_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/payment_screen.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart'; +import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart'; +import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart'; +import 'package:eatvio_app/features/create_plan/presentation/create_plan_screen.dart'; +import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart'; +import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart'; +import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart'; +import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart'; +import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; +import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart'; +import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart'; +import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart'; +import 'package:eatvio_app/features/support/presentation/pages/support_screen.dart'; +import 'package:eatvio_app/features/update_screen/presentation/update_screen.dart'; +import 'package:eatvio_app/features/user_details/presentation/user_details_screen.dart'; +import 'package:eatvio_app/features/welcome/presentation/pages/welcome_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart'; +import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:version/version.dart'; + +/// Остальные маршруты приложения +class OtherRoutes { + /// Создание остальных маршрутов + static List createRoutes() { + return [ + // Welcome and update screens + GoRoute( + path: RouteConstants.welcomeScreen, + name: 'welcomeScreen', + builder: (context, state) => const WelcomeScreen(), + redirect: (context, state) async { + try { + final sharedPreferences = await SharedPreferences.getInstance(); + final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed') ?? false; + await sl().registerPushToken(); + if (welcomeScreenWasShowed == false) { + return null; + } else if (await TokenStorage.haveTokenInStorage()) { + return RouteConstants.feedScreen; + } else { + return RouteConstants.authScreen; + } + } catch (e) { + return RouteConstants.feedScreen; + } + }, + ), + GoRoute( + path: RouteConstants.updateScreen, + name: 'updateScreen', + builder: (context, state) => const UpdateScreen(), + redirect: (context, state) async { + try { + final versions = (await sl().get('$baseUrl/api/app_versions')).data; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + String version = packageInfo.version; + Version currentVersion = Version.parse(version); + Version latestVersion = Version.parse(versions['min_version']); + if (latestVersion > currentVersion) { + return null; + } else { + return RouteConstants.welcomeScreen; + } + } catch (e) { + return RouteConstants.welcomeScreen; + } + }, + ), + + // Archive routes + GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()), + GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()), + GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()), + + // Finance routes + GoRoute(path: RouteConstants.userFinanceScreen, name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()), + GoRoute( + path: CardSettingsScreen.path, + name: CardSettingsScreen.name, + builder: (context, state) { + final chosenPlanId = state.extra as int?; + return CardSettingsScreen(chosenPlanId: chosenPlanId); + }, + ), + + // Content creation routes + GoRoute( + path: RecipeCreateScreen.path, + name: RecipeCreateScreen.name, + builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null), + ), + GoRoute( + path: CreateDietScreen.path, + name: CreateDietScreen.name, + builder: (context, state) { + final diet = state.extra as Diet?; + return CreateDietScreen(diet: diet); + }, + ), + GoRoute( + path: DietPreviewScreen.path, + name: DietPreviewScreen.name, + builder: (context, state) { + final data = state.extra as Diet; + return DietPreviewScreen(diet: data); + }, + ), + GoRoute(path: RouteConstants.createPlanScreen, name: 'createPlanScreen', builder: (context, state) => const CreatePlanScreen()), + GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()), + GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()), + GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()), + + // Profile routes + GoRoute( + path: RouteConstants.profileScreen, + name: 'profileScreen', + builder: (context, state) => ProfileScreen(author: state.extra as Author), + ), + GoRoute( + path: RouteConstants.subsScreen, + name: 'subsScreen', + builder: (context, state) => SubsScreen(slug: state.pathParameters['slug']!, subType: state.extra as SubType), + ), + + // Chat routes + GoRoute( + path: RouteConstants.chatScreen, + name: 'chatScreen', + builder: (context, state) { + final data = state.extra as Map; + final uuid = data['uuid'] as String; + final sendInitialMessage = data['sendInitialMessage'] as bool?; + return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage); + }, + ), + GoRoute( + path: RouteConstants.emptyChatScreen, + name: 'emptyChatScreen', + builder: (context, state) => EmptyChatScreen(author: state.extra as Author), + ), + + // Consultant routes + GoRoute(path: RouteConstants.becomeConsultantScreen, name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()), + GoRoute( + path: RouteConstants.stepperBecomeConsultantScreen, + name: 'stepperBecomeConsultantScreen', + builder: (context, state) => StepperBecomeConsultantScreen(slug: state.extra == null ? null : state.extra as String), + ), + GoRoute(path: RouteConstants.calendarScreen, name: 'calendarScreen', builder: (context, state) => const CalendarScreen()), + GoRoute(path: RouteConstants.supportScreen, name: 'supportScreen', builder: (context, state) => const SupportScreen()), + GoRoute( + path: RouteConstants.photoViewScreen, + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen(imageUrls: state.extra as List, index: int.parse(state.pathParameters['index']!)), + ), + + // Other routes + GoRoute(path: RouteConstants.glossaryScreen, name: 'glossaryScreen', builder: (context, state) => const GlossaryScreen()), + GoRoute(path: RouteConstants.blockedUsersScreen, name: 'blockedUsersScreen', builder: (context, state) => const BlockedUsersScreen()), + GoRoute( + path: RouteConstants.notificationsSettingsScreen, + name: 'notificationsSettingsScreen', + builder: (context, state) => const NotificationsSettingsScreen(), + ), + GoRoute( + path: UserDetailsScreen.path, + name: UserDetailsScreen.name, + builder: (context, state) => UserDetailsScreen(slug: state.extra as String), + ), + GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()), + GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()), + GoRoute( + path: InfoByOrderScreen.path, + name: InfoByOrderScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final successfulPayment = data['successfulPayment'] as bool?; + return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment); + }, + ), + GoRoute( + path: PaymentScreen.path, + name: PaymentScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final planId = data['planId'] as int; + final url = data['url'] as String; + return PaymentScreen(url: url, planId: planId); + }, + ), + GoRoute( + path: UserDiaryScreen.path, + name: UserDiaryScreen.name, + builder: (context, state) { + final data = state.extra as Map; + final diaryId = data['diaryId'] as int; + final currentPlanDay = data['currentDiaryDay'] as int; + return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay); + }, + ), + ]; + } +} diff --git a/lib/core/routing/route_constants.dart b/lib/core/routing/route_constants.dart new file mode 100644 index 00000000..dcc82c17 --- /dev/null +++ b/lib/core/routing/route_constants.dart @@ -0,0 +1,84 @@ +/// Константы маршрутов приложения +class RouteConstants { + // Auth routes + static const String authScreen = '/auth-screen'; + static const String registrationScreen = 'registration-screen'; + static const String forgetPasswordScreen = 'forget-password-screen'; + static const String scanQRCodeScreen = 'scan-qr-code-screen'; + static const String verifyScreen = '/verify-screen'; + static const String verifyEmailScreen = '/verify-email-screen'; + + // Main navigation routes + static const String myProfileScreen = '/my-profile-screen'; + static const String feedScreen = '/feed-screen'; + static const String diaryScreen = '/diary-screen'; + static const String consultationsScreen = '/consultations-screen'; + static const String chatSelectScreen = '/chat-select-screen'; + static const String notificationsScreen = '/notifications-screen'; + static const String menuScreen = '/menu-screen'; + + // Content routes + static const String aboutPlanScreen = '/about-plan-screen'; + static const String aboutRecipesScreen = '/about-recipes-screen'; + static const String aboutDietScreen = '/about-diet-screen'; + static const String blogPostScreen = '/blog-post-screen'; + static const String blogPostCreateScreen = '/blog-post-create-screen'; + + // Profile routes + static const String profileScreen = '/profile-screen'; + static const String subsScreen = '/subs-screen'; + + // Settings routes + static const String settingsScreen = '/settings-screen'; + static const String generalInfoScreen = 'general-info-screen'; + static const String otherInfoScreen = 'other-info-screen'; + static const String physicalActivityScreen = 'physical-activity-screen'; + static const String physicalFormScreen = 'physical-form-screen'; + static const String pressureScreen = 'pressure-screen'; + static const String changePasswordScreen = 'change-password-screen'; + static const String changeEmailScreen = 'change-email-screen'; + + // Consultant routes + static const String becomeConsultantScreen = '/become-consultant-screen'; + static const String stepperBecomeConsultantScreen = '/stepper-become-consultant-screen'; + static const String consultantRatingScreen = '/consultant-rating-screen'; + + // Other routes + static const String welcomeScreen = '/welcome-screen'; + static const String updateScreen = '/update-screen'; + static const String calendarScreen = '/calendar-screen'; + static const String supportScreen = '/support-screen'; + static const String photoViewScreen = '/photo-view-screen'; + static const String chatScreen = '/chat-screen'; + static const String emptyChatScreen = '/empty-chat-screen'; + static const String glossaryScreen = '/glossary-screen'; + static const String blockedUsersScreen = '/blocked-users-screen'; + static const String notificationsSettingsScreen = '/notifications-settings-screen'; + + // Finance routes + static const String userFinanceScreen = '/user-finance-screen'; + static const String cardSettingsScreen = '/card-settings-screen'; + static const String orderInfoScreen = '/order-info-screen'; + static const String paymentScreen = '/payment-screen'; + + // Content creation routes + static const String recipeCreateScreen = '/recipe-create-screen'; + static const String createDietScreen = '/create-diet-screen'; + static const String dietPreviewScreen = '/diet-preview-screen'; + static const String createPlanScreen = '/create-plan-screen'; + static const String myDietsScreen = '/my-diets-screen'; + static const String myPlansScreen = '/my-plans-screen'; + static const String myRecipesScreen = '/my-recipes-screen'; + + // User details + static const String userDetailsScreen = '/user-details-screen'; + static const String userDiaryScreen = '/user-diary-screen'; + + // Dev tools + static const String devToolsScreen = '/dev-tools-screen'; + + // Archive routes + static const String consultationsArchiveScreen = '/consultations-archive-screen'; + static const String consultantFinancesScreen = '/consultant-finances-screen'; + static const String userPurchasesScreen = '/user-purchases-screen'; +} diff --git a/lib/core/routing/settings_routes.dart b/lib/core/routing/settings_routes.dart new file mode 100644 index 00000000..8316617c --- /dev/null +++ b/lib/core/routing/settings_routes.dart @@ -0,0 +1,62 @@ +import 'package:eatvio_app/core/routing/route_constants.dart'; +import 'package:eatvio_app/features/settings/entities/settings_entity.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/change_password_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/general_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/other_info_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_activity_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/physical_form_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/pressure_screen.dart'; +import 'package:eatvio_app/features/settings/presentation/pages/settings_screen.dart'; +import 'package:go_router/go_router.dart'; + +/// Маршруты для настроек +class SettingsRoutes { + /// Создание маршрутов настроек + static List createRoutes() { + return [ + GoRoute( + path: RouteConstants.settingsScreen, + name: 'settingsScreen', + builder: (context, state) => const SettingsScreen(), + routes: [ + GoRoute( + path: RouteConstants.generalInfoScreen, + name: 'generalInfoScreen', + builder: (context, state) => GeneralInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.otherInfoScreen, + name: 'otherInfoScreen', + builder: (context, state) => OtherInfoScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalActivityScreen, + name: 'physicalActivityScreen', + builder: (context, state) => PhysicalActivityScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.physicalFormScreen, + name: 'physicalFormScreen', + builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.pressureScreen, + name: 'pressureScreen', + builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changePasswordScreen, + name: 'changePasswordScreen', + builder: (context, state) => ChangePasswordScreen(settingsEntity: state.extra as SettingsEntity), + ), + GoRoute( + path: RouteConstants.changeEmailScreen, + name: 'changeEmailScreen', + builder: (context, state) => ChangeEmailScreen(settingsEntity: state.extra as SettingsEntity), + ), + ], + ), + ]; + } +} diff --git a/lib/core/utils/validation_utils.dart b/lib/core/utils/validation_utils.dart new file mode 100644 index 00000000..1add6cda --- /dev/null +++ b/lib/core/utils/validation_utils.dart @@ -0,0 +1,87 @@ +import 'package:eatvio_app/core/constants/app_constants.dart'; + +/// Утилиты для валидации +class ValidationUtils { + /// Валидация email + static bool isValidEmail(String email) { + if (email.isEmpty) return false; + final regex = RegExp(AppConstants.emailRegex); + return regex.hasMatch(email); + } + + /// Валидация телефона + static bool isValidPhone(String phone) { + if (phone.isEmpty) return false; + final regex = RegExp(AppConstants.phoneRegex); + return regex.hasMatch(phone); + } + + /// Валидация URL + static bool isValidUrl(String url) { + if (url.isEmpty) return false; + final regex = RegExp(AppConstants.urlRegex); + return regex.hasMatch(url); + } + + /// Валидация длины текста + static bool isValidTextLength(String text, {int? maxLength}) { + final length = maxLength ?? AppConstants.maxTextLength; + return text.length <= length; + } + + /// Валидация длины заголовка + static bool isValidTitleLength(String title) { + return title.isNotEmpty && title.length <= AppConstants.maxTitleLength; + } + + /// Валидация длины описания + static bool isValidDescriptionLength(String description) { + return description.length <= AppConstants.maxDescriptionLength; + } + + /// Валидация пароля + static bool isValidPassword(String password) { + // Минимум 8 символов, хотя бы одна буква и одна цифра + if (password.length < 8) return false; + final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password); + final hasDigit = RegExp(r'[0-9]').hasMatch(password); + return hasLetter && hasDigit; + } + + /// Валидация имени пользователя + static bool isValidUsername(String username) { + // От 3 до 20 символов, только буквы, цифры и подчеркивания + if (username.length < 3 || username.length > 20) return false; + final regex = RegExp(r'^[a-zA-Z0-9_]+$'); + return regex.hasMatch(username); + } + + /// Получение сообщения об ошибке валидации + static String? getValidationErrorMessage(String? value, ValidationType type) { + if (value == null || value.isEmpty) { + return 'Поле не может быть пустым'; + } + + switch (type) { + case ValidationType.email: + return isValidEmail(value) ? null : 'Введите корректный email'; + case ValidationType.phone: + return isValidPhone(value) ? null : 'Введите корректный номер телефона'; + case ValidationType.url: + return isValidUrl(value) ? null : 'Введите корректный URL'; + case ValidationType.password: + return isValidPassword(value) ? null : 'Пароль должен содержать минимум 8 символов, включая буквы и цифры'; + case ValidationType.username: + return isValidUsername(value) ? null : 'Имя пользователя должно содержать от 3 до 20 символов (буквы, цифры, подчеркивания)'; + case ValidationType.title: + return isValidTitleLength(value) ? null : 'Заголовок не может быть пустым и должен содержать не более ${AppConstants.maxTitleLength} символов'; + case ValidationType.description: + return isValidDescriptionLength(value) ? null : 'Описание не может содержать более ${AppConstants.maxDescriptionLength} символов'; + case ValidationType.text: + return isValidTextLength(value) ? null : 'Текст не может содержать более ${AppConstants.maxTextLength} символов'; + } + } +} + +/// Типы валидации +enum ValidationType { email, phone, url, password, username, title, description, text } diff --git a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart index df228298..6e93944a 100644 --- a/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart +++ b/lib/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart @@ -1,61 +1,34 @@ -import 'dart:developer'; - +import 'package:eatvio_app/core/cubit/base_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; -class BlogPostCreateCubit extends Cubit { +class BlogPostCreateCubit extends BaseCubit { final BlogPostRepository blogPostRepository; - BlogPostCreateCubit({ - required this.blogPostRepository, - }) : super(const BlogPostCreateState()); + BlogPostCreateCubit({required this.blogPostRepository}) : super(const BlogPostCreateState()); - Future createBlogPost({ - String? title, - String? videoUrl, - String? description, - XFile? image, - }) async { + Future createBlogPost({String? title, String? videoUrl, String? description, XFile? image}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.createBlogPost( - title, - videoUrl, - description, - image, - ); + await blogPostRepository.createBlogPost(title, videoUrl, description, image); + emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } } - Future updateBlogPost({ - required int id, - String? title, - String? videoUrl, - String? description, - XFile? image, - String? oldImageUrl, - }) async { + Future updateBlogPost({required int id, String? title, String? videoUrl, String? description, XFile? image, String? oldImageUrl}) async { try { emit(const BlogPostCreateState.loading()); - await blogPostRepository.updateBlogPost( - id, - title, - videoUrl, - description, - image, - oldImageUrl, - ); + await blogPostRepository.updateBlogPost(id, title, videoUrl, description, image, oldImageUrl); emit(const BlogPostCreateState.loaded()); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); emit(const BlogPostCreateState.loaded()); return false; } @@ -65,8 +38,8 @@ class BlogPostCreateCubit extends Cubit { try { await blogPostRepository.deleteBlogPost(id); return true; - } catch (e, st) { - log(e.toString(), stackTrace: st); + } catch (error, stackTrace) { + handleError(error, stackTrace); return false; } }