diff --git a/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml b/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml index 6561fcc..cd0a3f9 100644 --- a/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml +++ b/StudioProjects/yimaru_app/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + + + + Terms and Condition + + + + + + +
+ +
+
+
Terms and Condition
+
+ +
Last updated: October 26, 2025
+ + +

Introduction

+

+ Welcome to Yimaru! These terms and conditions outline the rules and + regulations for the use of our application. By accessing this app, + we assume you accept these terms and conditions. +

+ + +

User Accounts

+

+ When you create an account with us, you must provide us with information + that is accurate, complete, and current at all times. Failure to do so + constitutes a breach of the Terms, which may result in immediate + termination of your account on our Service. +

+ + +

Content & Services

+

+ Our Service allows you to access learning materials. You are granted a + limited license to access and use the app content for personal, + non-commercial purposes. You agree not to: +

+ +
    +
  • Reproduce, duplicate, copy, or sell any material from the app.
  • +
  • Redistribute content from Yimaru.
  • +
  • Use the app in any way that is damaging or harmful.
  • +
+ + +

Privacy Policy

+

+ Your privacy is important to us. Please read our + Privacy Policy to understand how we collect, use, + and share information about you. +

+ + +

Contact Us

+

+ If you have any questions about these Terms, please contact us at + support@yimaru.et. +

+ +
+
+ + + + + + diff --git a/StudioProjects/yimaru_app/assets/icons/flag.png b/StudioProjects/yimaru_app/assets/icons/flag.png new file mode 100644 index 0000000..b0c75c7 Binary files /dev/null and b/StudioProjects/yimaru_app/assets/icons/flag.png differ diff --git a/StudioProjects/yimaru_app/assets/icons/google.png b/StudioProjects/yimaru_app/assets/icons/google.png new file mode 100644 index 0000000..c44c633 Binary files /dev/null and b/StudioProjects/yimaru_app/assets/icons/google.png differ diff --git a/StudioProjects/yimaru_app/assets/icons/logo.svg b/StudioProjects/yimaru_app/assets/icons/logo.svg index d6ffb10..58ec1a4 100644 --- a/StudioProjects/yimaru_app/assets/icons/logo.svg +++ b/StudioProjects/yimaru_app/assets/icons/logo.svg @@ -1,15 +1,24 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/StudioProjects/yimaru_app/assets/images/coming_soon.png b/StudioProjects/yimaru_app/assets/images/coming_soon.png new file mode 100644 index 0000000..0bc8cb8 Binary files /dev/null and b/StudioProjects/yimaru_app/assets/images/coming_soon.png differ diff --git a/StudioProjects/yimaru_app/assets/images/image_1.png b/StudioProjects/yimaru_app/assets/images/image_1.png new file mode 100644 index 0000000..6eab34f Binary files /dev/null and b/StudioProjects/yimaru_app/assets/images/image_1.png differ diff --git a/StudioProjects/yimaru_app/assets/images/profile.png b/StudioProjects/yimaru_app/assets/images/profile.png new file mode 100644 index 0000000..7d74c01 Binary files /dev/null and b/StudioProjects/yimaru_app/assets/images/profile.png differ diff --git a/StudioProjects/yimaru_app/lib/app/app.dart b/StudioProjects/yimaru_app/lib/app/app.dart index c6a337a..a8ba819 100644 --- a/StudioProjects/yimaru_app/lib/app/app.dart +++ b/StudioProjects/yimaru_app/lib/app/app.dart @@ -5,7 +5,27 @@ import 'package:stacked/stacked_annotations.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart'; import 'package:yimaru_app/ui/views/startup/startup_view.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; +import 'package:yimaru_app/ui/views/profile/profile_view.dart'; +import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart'; +import 'package:yimaru_app/ui/views/downloads/downloads_view.dart'; +import 'package:yimaru_app/ui/views/progress/progress_view.dart'; +import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart'; +import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart'; +import 'package:yimaru_app/ui/views/support/support_view.dart'; +import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart'; +import 'package:yimaru_app/ui/views/call_support/call_support_view.dart'; +import 'package:yimaru_app/ui/views/language/language_view.dart'; +import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart'; +import 'package:yimaru_app/ui/views/terms_and_conditions/terms_and_conditions_view.dart'; +import 'package:yimaru_app/ui/views/register/register_view.dart'; +import 'package:yimaru_app/ui/views/login/login_view.dart'; +import 'package:yimaru_app/ui/views/learn/learn_view.dart'; +import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart'; +import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; +import 'package:yimaru_app/services/dio_service.dart'; // @stacked-import @StackedApp( @@ -13,13 +33,34 @@ import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; MaterialRoute(page: HomeView), MaterialRoute(page: OnboardingView), MaterialRoute(page: StartupView), + MaterialRoute(page: ProfileView), + MaterialRoute(page: ProfileDetailView), + MaterialRoute(page: DownloadsView), + MaterialRoute(page: ProgressView), + MaterialRoute(page: OngoingProgressView), + MaterialRoute(page: AccountPrivacyView), + MaterialRoute(page: SupportView), + MaterialRoute(page: TelegramSupportView), + MaterialRoute(page: CallSupportView), + MaterialRoute(page: LanguageView), + MaterialRoute(page: PrivacyPolicyView), + MaterialRoute(page: TermsAndConditionsView), + MaterialRoute(page: RegisterView), + MaterialRoute(page: LoginView), + MaterialRoute(page: LearnView), + MaterialRoute(page: LearnLevelView), + MaterialRoute(page: LearnModuleView), // @stacked-route ], dependencies: [ LazySingleton(classType: BottomSheetService), LazySingleton(classType: DialogService), LazySingleton(classType: NavigationService), - // @stacked-service + LazySingleton(classType: AuthenticationService), + LazySingleton(classType: ApiService), + LazySingleton(classType: SecureStorageService), + LazySingleton(classType: DioService), +// @stacked-service ], bottomsheets: [ StackedBottomsheet(classType: NoticeSheet), diff --git a/StudioProjects/yimaru_app/lib/app/app.locator.dart b/StudioProjects/yimaru_app/lib/app/app.locator.dart index 9d4a8da..471c929 100644 --- a/StudioProjects/yimaru_app/lib/app/app.locator.dart +++ b/StudioProjects/yimaru_app/lib/app/app.locator.dart @@ -11,6 +11,11 @@ import 'package:stacked_services/src/dialog/dialog_service.dart'; import 'package:stacked_services/src/navigation/navigation_service.dart'; import 'package:stacked_shared/stacked_shared.dart'; +import '../services/api_service.dart'; +import '../services/authentication_service.dart'; +import '../services/dio_service.dart'; +import '../services/secure_storage_service.dart'; + final locator = StackedLocator.instance; Future setupLocator({ @@ -25,4 +30,8 @@ Future setupLocator({ locator.registerLazySingleton(() => BottomSheetService()); locator.registerLazySingleton(() => DialogService()); locator.registerLazySingleton(() => NavigationService()); + locator.registerLazySingleton(() => AuthenticationService()); + locator.registerLazySingleton(() => ApiService()); + locator.registerLazySingleton(() => SecureStorageService()); + locator.registerLazySingleton(() => DioService()); } diff --git a/StudioProjects/yimaru_app/lib/app/app.router.dart b/StudioProjects/yimaru_app/lib/app/app.router.dart index b3725f5..f2872b4 100644 --- a/StudioProjects/yimaru_app/lib/app/app.router.dart +++ b/StudioProjects/yimaru_app/lib/app/app.router.dart @@ -5,14 +5,38 @@ // ************************************************************************** // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:flutter/material.dart' as _i6; +import 'package:flutter/material.dart' as _i22; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart' as _i1; -import 'package:stacked_services/stacked_services.dart' as _i7; +import 'package:stacked_services/stacked_services.dart' as _i23; +import 'package:yimaru_app/ui/views/account_privacy/account_privacy_view.dart' + as _i10; +import 'package:yimaru_app/ui/views/call_support/call_support_view.dart' + as _i13; +import 'package:yimaru_app/ui/views/downloads/downloads_view.dart' as _i7; import 'package:yimaru_app/ui/views/home/home_view.dart' as _i2; -import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart' as _i5; +import 'package:yimaru_app/ui/views/language/language_view.dart' as _i14; +import 'package:yimaru_app/ui/views/learn/learn_view.dart' as _i19; +import 'package:yimaru_app/ui/views/learn_level/learn_level_view.dart' as _i20; +import 'package:yimaru_app/ui/views/learn_module/learn_module_view.dart' + as _i21; +import 'package:yimaru_app/ui/views/login/login_view.dart' as _i18; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.dart' as _i3; +import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_view.dart' + as _i9; +import 'package:yimaru_app/ui/views/privacy_policy/privacy_policy_view.dart' + as _i15; +import 'package:yimaru_app/ui/views/profile/profile_view.dart' as _i5; +import 'package:yimaru_app/ui/views/profile_detail/profile_detail_view.dart' + as _i6; +import 'package:yimaru_app/ui/views/progress/progress_view.dart' as _i8; +import 'package:yimaru_app/ui/views/register/register_view.dart' as _i17; import 'package:yimaru_app/ui/views/startup/startup_view.dart' as _i4; +import 'package:yimaru_app/ui/views/support/support_view.dart' as _i11; +import 'package:yimaru_app/ui/views/telegram_support/telegram_support_view.dart' + as _i12; +import 'package:yimaru_app/ui/views/terms_and_conditions/terms_and_conditions_view.dart' + as _i16; class Routes { static const homeView = '/home-view'; @@ -21,12 +45,61 @@ class Routes { static const startupView = '/startup-view'; + static const profileView = '/profile-view'; + static const profileDetailView = '/profile-detail-view'; + + static const downloadsView = '/downloads-view'; + + static const progressView = '/progress-view'; + + static const ongoingProgressView = '/ongoing-progress-view'; + + static const accountPrivacyView = '/account-privacy-view'; + + static const supportView = '/support-view'; + + static const telegramSupportView = '/telegram-support-view'; + + static const callSupportView = '/call-support-view'; + + static const languageView = '/language-view'; + + static const privacyPolicyView = '/privacy-policy-view'; + + static const termsAndConditionsView = '/terms-and-conditions-view'; + + static const registerView = '/register-view'; + + static const loginView = '/login-view'; + + static const learnView = '/learn-view'; + + static const learnLevelView = '/learn-level-view'; + + static const learnModuleView = '/learn-module-view'; static const all = { homeView, onboardingView, startupView, + profileView, + profileDetailView, + downloadsView, + progressView, + ongoingProgressView, + accountPrivacyView, + supportView, + telegramSupportView, + callSupportView, + languageView, + privacyPolicyView, + termsAndConditionsView, + registerView, + loginView, + learnView, + learnLevelView, + learnModuleView, }; } @@ -44,31 +117,194 @@ class StackedRouter extends _i1.RouterBase { Routes.startupView, page: _i4.StartupView, ), - + _i1.RouteDef( + Routes.profileView, + page: _i5.ProfileView, + ), + _i1.RouteDef( + Routes.profileDetailView, + page: _i6.ProfileDetailView, + ), + _i1.RouteDef( + Routes.downloadsView, + page: _i7.DownloadsView, + ), + _i1.RouteDef( + Routes.progressView, + page: _i8.ProgressView, + ), + _i1.RouteDef( + Routes.ongoingProgressView, + page: _i9.OngoingProgressView, + ), + _i1.RouteDef( + Routes.accountPrivacyView, + page: _i10.AccountPrivacyView, + ), + _i1.RouteDef( + Routes.supportView, + page: _i11.SupportView, + ), + _i1.RouteDef( + Routes.telegramSupportView, + page: _i12.TelegramSupportView, + ), + _i1.RouteDef( + Routes.callSupportView, + page: _i13.CallSupportView, + ), + _i1.RouteDef( + Routes.languageView, + page: _i14.LanguageView, + ), + _i1.RouteDef( + Routes.privacyPolicyView, + page: _i15.PrivacyPolicyView, + ), + _i1.RouteDef( + Routes.termsAndConditionsView, + page: _i16.TermsAndConditionsView, + ), + _i1.RouteDef( + Routes.registerView, + page: _i17.RegisterView, + ), + _i1.RouteDef( + Routes.loginView, + page: _i18.LoginView, + ), + _i1.RouteDef( + Routes.learnView, + page: _i19.LearnView, + ), + _i1.RouteDef( + Routes.learnLevelView, + page: _i20.LearnLevelView, + ), + _i1.RouteDef( + Routes.learnModuleView, + page: _i21.LearnModuleView, + ), ]; final _pagesMap = { _i2.HomeView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i2.HomeView(), settings: data, ); }, _i3.OnboardingView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i3.OnboardingView(), settings: data, ); }, _i4.StartupView: (data) { - return _i6.MaterialPageRoute( + return _i22.MaterialPageRoute( builder: (context) => const _i4.StartupView(), settings: data, ); }, - _i5.LanguageSelector: (data) { - return _i6.MaterialPageRoute( - builder: (context) => const _i5.LanguageSelector(), + _i5.ProfileView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i5.ProfileView(), + settings: data, + ); + }, + _i6.ProfileDetailView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i6.ProfileDetailView(), + settings: data, + ); + }, + _i7.DownloadsView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i7.DownloadsView(), + settings: data, + ); + }, + _i8.ProgressView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i8.ProgressView(), + settings: data, + ); + }, + _i9.OngoingProgressView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i9.OngoingProgressView(), + settings: data, + ); + }, + _i10.AccountPrivacyView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i10.AccountPrivacyView(), + settings: data, + ); + }, + _i11.SupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i11.SupportView(), + settings: data, + ); + }, + _i12.TelegramSupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i12.TelegramSupportView(), + settings: data, + ); + }, + _i13.CallSupportView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i13.CallSupportView(), + settings: data, + ); + }, + _i14.LanguageView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i14.LanguageView(), + settings: data, + ); + }, + _i15.PrivacyPolicyView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i15.PrivacyPolicyView(), + settings: data, + ); + }, + _i16.TermsAndConditionsView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i16.TermsAndConditionsView(), + settings: data, + ); + }, + _i17.RegisterView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i17.RegisterView(), + settings: data, + ); + }, + _i18.LoginView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i18.LoginView(), + settings: data, + ); + }, + _i19.LearnView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i19.LearnView(), + settings: data, + ); + }, + _i20.LearnLevelView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i20.LearnLevelView(), + settings: data, + ); + }, + _i21.LearnModuleView: (data) { + return _i22.MaterialPageRoute( + builder: (context) => const _i21.LearnModuleView(), settings: data, ); }, @@ -81,7 +317,7 @@ class StackedRouter extends _i1.RouterBase { Map get pagesMap => _pagesMap; } -extension NavigatorStateExtension on _i7.NavigationService { +extension NavigatorStateExtension on _i23.NavigationService { Future navigateToHomeView([ int? routerId, bool preventDuplicates = true, @@ -124,7 +360,243 @@ extension NavigatorStateExtension on _i7.NavigationService { transition: transition); } + Future navigateToProfileView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.profileView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future navigateToProfileDetailView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.profileDetailView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToDownloadsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.downloadsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.progressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToOngoingProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.ongoingProgressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToAccountPrivacyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.accountPrivacyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.supportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToTelegramSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.telegramSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToCallSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.callSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLanguageView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.languageView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToPrivacyPolicyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.privacyPolicyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToTermsAndConditionsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.termsAndConditionsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToRegisterView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.registerView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLoginView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.loginView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnLevelView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnLevelView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future navigateToLearnModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return navigateTo(Routes.learnModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } Future replaceWithHomeView([ int? routerId, @@ -168,5 +640,241 @@ extension NavigatorStateExtension on _i7.NavigationService { transition: transition); } + Future replaceWithProfileView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.profileView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + Future replaceWithProfileDetailView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.profileDetailView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithDownloadsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.downloadsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.progressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithOngoingProgressView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.ongoingProgressView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithAccountPrivacyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.accountPrivacyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.supportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithTelegramSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.telegramSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithCallSupportView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.callSupportView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLanguageView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.languageView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithPrivacyPolicyView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.privacyPolicyView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithTermsAndConditionsView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.termsAndConditionsView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithRegisterView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.registerView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLoginView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.loginView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnLevelView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnLevelView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } + + Future replaceWithLearnModuleView([ + int? routerId, + bool preventDuplicates = true, + Map? parameters, + Widget Function(BuildContext, Animation, Animation, Widget)? + transition, + ]) async { + return replaceWith(Routes.learnModuleView, + id: routerId, + preventDuplicates: preventDuplicates, + parameters: parameters, + transition: transition); + } } diff --git a/StudioProjects/yimaru_app/lib/main.dart b/StudioProjects/yimaru_app/lib/main.dart index d23ff42..ad534c7 100644 --- a/StudioProjects/yimaru_app/lib/main.dart +++ b/StudioProjects/yimaru_app/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:toastification/toastification.dart'; import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.locator.dart'; @@ -17,13 +18,17 @@ class MainApp extends StatelessWidget { const MainApp({super.key}); @override - Widget build(BuildContext context) { - return MaterialApp( - initialRoute: Routes.startupView, - theme: ThemeData(fontFamily: 'Aeonik'), - onGenerateRoute: StackedRouter().onGenerateRoute, - navigatorKey: StackedService.navigatorKey, - navigatorObservers: [StackedService.routeObserver], - ); - } + Widget build(BuildContext context) => _buildMaterialWrapper(); + + Widget _buildMaterialWrapper() => ToastificationWrapper( + child: _buildMaterialApp(), + ); + + Widget _buildMaterialApp() => MaterialApp( + initialRoute: Routes.startupView, + theme: ThemeData(fontFamily: 'Aeonik'), + onGenerateRoute: StackedRouter().onGenerateRoute, + navigatorKey: StackedService.navigatorKey, + navigatorObservers: [StackedService.routeObserver], + ); } diff --git a/StudioProjects/yimaru_app/lib/models/user_model.dart b/StudioProjects/yimaru_app/lib/models/user_model.dart new file mode 100644 index 0000000..deab289 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/models/user_model.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class UserModel { + @JsonKey(name: 'user_id') + final int? userId; + + @JsonKey(name: 'access_token') + final String? accessToken; + + @JsonKey(name: 'refresh_token') + final String? refreshToken; + + UserModel({this.userId, this.accessToken, this.refreshToken}); + + factory UserModel.fromJson(Map json) => + _$UserModelFromJson(json); + + Map toJson() => _$UserModelToJson(this); +} diff --git a/StudioProjects/yimaru_app/lib/models/user_model.g.dart b/StudioProjects/yimaru_app/lib/models/user_model.g.dart new file mode 100644 index 0000000..36e2c21 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/models/user_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserModel _$UserModelFromJson(Map json) => UserModel( + userId: (json['user_id'] as num?)?.toInt(), + accessToken: json['access_token'] as String?, + refreshToken: json['refresh_token'] as String?, + ); + +Map _$UserModelToJson(UserModel instance) => { + 'user_id': instance.userId, + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + }; diff --git a/StudioProjects/yimaru_app/lib/services/api_service.dart b/StudioProjects/yimaru_app/lib/services/api_service.dart new file mode 100644 index 0000000..047a5fc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/api_service.dart @@ -0,0 +1,171 @@ +import 'package:dio/dio.dart'; +import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/services/dio_service.dart'; +import 'package:yimaru_app/ui/common/app_constants.dart'; + +import '../app/app.locator.dart'; +import '../ui/common/enmus.dart'; + +class ApiService { + final _service = locator(); + + // Http headers + Map _getHeaders({String? token}) => { + + // if (token != null) 'Authorization': 'Bearer $token', + + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', + if (token != null) 'Authorization': 'Bearer $token' + }; + + // Dio options + Options? _getOptions({String? token}) { + return Options( + // followRedirects: false, + // validateStatus: (status) => true, + headers: _getHeaders(token: token), + ); + } + + // Register + Future> register(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kRegisterUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp sent successfully' + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': 'Unknown Error Occurred' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Login + Future> login(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$kLoginUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Logged in successfully', + 'data': UserModel.fromJson(response.data['data']), + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Verify otp + Future> verifyOtp(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kVerifyOtpUrl', + data: data, + options: _getOptions(), + ); + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp verified successfully', + //'data': UserModel.fromJson(response.data['data']), + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Resend otp + Future> resendOtp(Map data) async { + try { + Response response = await _service.dio.post( + '$baseUrl/$userUrl/$kResendOtpUrl', + data: data, + options: _getOptions(), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Otp resend successfully' + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': 'Unknown Error Occurred' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } + + // Profile completed + Future> getProfileStatus(UserModel? user) async { + try { + Response response = await _service.dio.get( + '$baseUrl/$userUrl/${user?.userId}/$kProfileStatusUrl', + options: _getOptions(token: user?.accessToken), + ); + + if (response.statusCode == 200) { + return { + 'status': ResponseStatus.success, + 'message': 'Profile completion status fetched successfully', + 'data': response.data['data']['is_profile_completed'] as bool, + }; + } else { + return { + 'status': ResponseStatus.failure, + 'message': '${response.data['message']}, ${response.data['error']}' + }; + } + } catch (e) { + return { + 'message': e.toString(), + 'status': ResponseStatus.failure, + }; + } + } +} diff --git a/StudioProjects/yimaru_app/lib/services/authentication_service.dart b/StudioProjects/yimaru_app/lib/services/authentication_service.dart new file mode 100644 index 0000000..131d687 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/authentication_service.dart @@ -0,0 +1,32 @@ +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/models/user_model.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; + +class AuthenticationService { + final _secureService = locator(); + + Future userLoggedIn() async { + if (await _secureService.getString('userId') != null) { + return true; + } + return false; + } + + Future saveUserData(Map data) async { + await _secureService.setInt('userId', data['userId']); + await _secureService.setString('accessToken', data['accessToken']); + await _secureService.setString('refreshToken', data['refreshToken']); + } + + Future getUser() async { + UserModel user = UserModel( + userId: await _secureService.getInt('userId'), + accessToken: await _secureService.getString('accessToken'), + refreshToken: await _secureService.getString('refreshToken')); + return user; + } + + Future logOut() async { + await _secureService.clear(); + } +} diff --git a/StudioProjects/yimaru_app/lib/services/dio_service.dart b/StudioProjects/yimaru_app/lib/services/dio_service.dart new file mode 100644 index 0000000..3a8eddf --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/dio_service.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; + +import '../ui/common/app_constants.dart'; + +class DioService { + final Dio _dio = Dio(); + + DioService() { + _dio.options.baseUrl = baseUrl; + _dio.options.connectTimeout = const Duration(seconds: 30); + _dio.options.receiveTimeout = const Duration(seconds: 30); + + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + debugPrint('➡️➡️➡️ REQUEST ➡️➡️➡️'); + debugPrint('➡️ Data: ${options.data}'); + debugPrint('➡️ Headers: ${options.headers}'); + debugPrint('➡️ ${options.method} ${options.uri}'); + handler.next(options); + }, + onResponse: (response, handler) { + debugPrint('✅✅✅ RESPONSE ✅✅✅'); + debugPrint('✅ Data : ${response.data}'); + debugPrint('✅ Status Code : ${response.statusCode}'); + handler.next(response); + }, + onError: (error, handler) { + debugPrint('❌❌❌ ERROR ❌❌❌'); + debugPrint('❌ ${error.message}'); + debugPrint('❌ URI: ${error.requestOptions.uri}'); + debugPrint('❌ Headers sent: ${error.requestOptions.headers}'); + handler.next(error); + }, + ), + ); + } + + Dio get dio => _dio; +} diff --git a/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart b/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart new file mode 100644 index 0000000..68fd4ac --- /dev/null +++ b/StudioProjects/yimaru_app/lib/services/secure_storage_service.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +extension BoolParsing on String { + bool parseBool() { + if (toLowerCase() == 'true') { + return true; + } else if (toLowerCase() == 'false') { + return false; + } + + throw '"$this" can not be parsed to boolean.'; + } +} + +class SecureStorageService { + // Create storage + + late final FlutterSecureStorage _storage; + + SecureStorageService() { + _storage = Platform.isAndroid + ? const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + ) + : const FlutterSecureStorage( + iOptions: IOSOptions(), + ); + } + + Future clear() async { + _storage.deleteAll(); + } + + Future getBool(String key) async { + String? result = await _storage.read(key: key); + return result?.parseBool(); + } + + Future getString(String key) async { + return await _storage.read(key: key); + } + + Future getInt(String key) async { + return await _storage.read(key: key) == null + ? null + : int.parse(await _storage.read(key: key) ?? '0'); + } + + Future setString(String key, String value) async { + await _storage.write(key: key, value: value); + } + + Future setInt(String key, int value) async { + await _storage.write(key: key, value: value.toString()); + } + + Future setBool(String key, bool value) async { + await _storage.write(key: key, value: value.toString()); + } +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart b/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart index b9b1b22..f7d0acb 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/app_colors.dart @@ -1,10 +1,19 @@ import 'package:flutter/material.dart'; -const Color kcBackgroundColor = kcWhiteColor; -const Color kcWhiteColor = Color(0xFFFFFFFF); +const Color kcBlack = Colors.black; +const Color kcRed = Color(0xffFF4C4C); +const Color kcGreen = Color(0xFF1DE964); +const Color kcBackgroundColor = kcWhite; +const Color kcWhite = Color(0xFFFFFFFF); +const Color kcIndigo = Color(0xff6A1B9A); +const Color kcOrange = Color(0xFFF79400); +const Color kcSkyBlue = Color(0xFF28B4CD); +const Color kcDarkGrey = Color(0xFF1A1B1E); const Color kcMediumGrey = Color(0xFF474A54); +const Color kcAquamarine = Color(0xFF1DE9B6); +const Color kcTransparent = Colors.transparent; const Color kcPrimaryColor = Color(0xFF9E2891); -const Color kcDarkGreyColor = Color(0xFF1A1B1E); +const Color kcPrimaryAccent = Color(0xFF6A1B9A); const Color kcVeryLightGrey = Color(0xFFE3E3E3); const Color kcPrimaryColorDark = Color(0xFF300151); const Color kcPrimaryColorLight = Color(0x149E2891); diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart b/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart new file mode 100644 index 0000000..69839b0 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/app_constants.dart @@ -0,0 +1,14 @@ +//String baseUrl = 'http://195.35.29.82:8080'; +String baseUrl = 'https://api.yimaru.yaltopia.com'; + +String userUrl = 'api/v1/user'; + +String kRegisterUrl = 'register'; + +String kVerifyOtpUrl = 'verify-otp'; + +String kResendOtpUrl = 'resend-otp'; + +String kLoginUrl = 'api/v1/auth/customer-login'; + +String kProfileStatusUrl = 'is-profile-completed'; diff --git a/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart b/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart index 06e7c8f..5cc0474 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/app_strings.dart @@ -1,3 +1,54 @@ +const String ksSuggestion = + "15 minutes a day can make you 3x more fluent in 3 month"; const String ksHomeBottomSheetTitle = 'Build Great Apps!'; +const String ksPrivacyPolicy = + 'A brief, simple overview of Yimaru’s commitment to user privacy. Our goal is to be transparent about the data we collect and how we use it to enhance your learning experience.'; const String ksHomeBottomSheetDescription = 'Stacked is built to help you build better apps. Give us a chance and we\'ll prove it to you. Check out stacked.filledstacks.com to learn more'; + +const String ksTerms = """ +

+Last updated: October 26, 2025 +

+ +

Introduction

+

+Welcome to Yimaru! These terms and conditions outline the rules and regulations +for the use of our application. By accessing this app, we assume you accept +these terms and conditions. +

+ +

User Accounts

+

+When you create an account with us, you must provide us with information that is +accurate, complete, and current at all times. Failure to do so constitutes a +breach of the Terms, which may result in immediate termination of your account +on our Service. +

+ +

Content & Services

+

+Our Service allows you to access learning materials. You are granted a limited +license to access and use the app content for personal, non-commercial purposes. +You agree not to: +

+ +
    +
  • Reproduce, duplicate, copy, or sell any material from the app.
  • +
  • Redistribute content from Yimaru.
  • +
  • Use the app in any way that is damaging or harmful.
  • +
+ +

Privacy Policy

+

+Your privacy is important to us. Please read our +Privacy Policy +to understand how we collect, use, and share information about you. +

+ +

Contact Us

+

+If you have any questions about these Terms, please contact us at +support@yimaru.et. +

+"""; diff --git a/StudioProjects/yimaru_app/lib/ui/common/enmus.dart b/StudioProjects/yimaru_app/lib/ui/common/enmus.dart new file mode 100644 index 0000000..10ab36e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/enmus.dart @@ -0,0 +1,7 @@ +// Registration type +enum RegistrationType { phone, email } + +// Report status +enum ResponseStatus { success, failure } + +enum LearnLevelStatus { pending, started, completed } diff --git a/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart b/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart index 1cf232c..b159372 100644 --- a/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart +++ b/StudioProjects/yimaru_app/lib/ui/common/ui_helpers.dart @@ -1,6 +1,9 @@ import 'dart:math'; - +import 'package:flutter_html/flutter_html.dart'; +import 'package:intl/intl.dart'; import 'package:flutter/material.dart'; +import 'package:pinput/pinput.dart'; +import 'package:toastification/toastification.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; const double _tinySize = 5.0; @@ -34,6 +37,9 @@ double screenWidth(BuildContext context) => MediaQuery.of(context).size.width; double screenHeight(BuildContext context) => MediaQuery.of(context).size.height; +double buttonAlignment(BuildContext context) => + MediaQuery.of(context).size.height * 0.9; + double screenHeightFraction( BuildContext context, { int dividedBy = 1, @@ -92,18 +98,20 @@ double getResponsiveFontSize( return responsiveSize; } -InputDecoration inputDecoration({ - String? hint, - required bool focus, -}) => +InputDecoration inputDecoration( + {String? hint, + Widget? suffix, + required bool focus, + required bool filled}) => InputDecoration( - hintText: hint, filled: true, + hintText: hint, border: border, + suffixIcon: suffix, errorBorder: errorBorder, enabledBorder: enabledBorder, focusedBorder: focusedBorder, - fillColor: focus ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + fillColor: focus || filled ? kcPrimaryColor.withOpacity(0.1) : kcWhite, ); Border rightBorder = Border( @@ -113,6 +121,8 @@ Border rightBorder = Border( ), ); +DateFormat format = DateFormat("d MMM, yyyy"); + OutlineInputBorder border = const OutlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); OutlineInputBorder errorBorder = @@ -124,3 +134,142 @@ OutlineInputBorder focusedBorder = UnderlineInputBorder searchBorder = const UnderlineInputBorder(borderSide: BorderSide(color: kcPrimaryColor)); + +TextStyle defaultPinTextStyle = const TextStyle( + fontSize: 22, + color: kcDarkGrey, +); + +BoxDecoration defaultPinDecoration = BoxDecoration( + borderRadius: BorderRadius.circular(19), + border: Border.all( + color: kcPrimaryColor.withOpacity(0.5), + ), +); + +PinTheme defaultPin = PinTheme( + width: 56, + height: 56, + textStyle: defaultPinTextStyle, + decoration: defaultPinDecoration, +); + +PinTheme focusedThemePin = defaultPin.copyWith( + decoration: defaultPin.decoration?.copyWith( + border: Border.all(color: kcPrimaryColor, width: 3), + ), +); + +PinTheme submittedThemePin = defaultPin.copyWith( + decoration: defaultPin.decoration?.copyWith( + borderRadius: BorderRadius.circular(19), + border: Border.all(color: kcPrimaryColor), + ), +); + +PinTheme errorPinTheme = defaultPin.copyBorderWith( + border: Border.all(color: Colors.red), +); + +TextStyle validationStyle = const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, +); + +TextStyle style25DG600 = const TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, +); + +TextStyle style12R700 = const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, +); + +TextStyle style16DG600 = const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, +); + +TextStyle style16DG400 = const TextStyle( + fontSize: 16, + color: kcDarkGrey, +); + +TextStyle style14DG400 = const TextStyle( + color: kcDarkGrey, +); + +TextStyle style14P400 = const TextStyle( + color: kcPrimaryColor, +); + +TextStyle style14P600 = const TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, +); + +Style htmlDefaultStyle = Style(color: kcDarkGrey, fontSize: FontSize(16)); + +Map htmlStyle = { + "p": htmlDefaultStyle, + "h1": htmlDefaultStyle, + "h2": htmlDefaultStyle, + "h3": htmlDefaultStyle, + "h4": htmlDefaultStyle, + "h5": htmlDefaultStyle, + "h6": htmlDefaultStyle, + "li": Style( + color: kcDarkGrey, + margin: Margins.zero, + fontSize: FontSize(16), + padding: HtmlPaddings.zero, + fontWeight: FontWeight.w400, + listStyleType: ListStyleType.circle, + verticalAlign: VerticalAlign.baseline, + ), +}; + +Widget buildToastDescription(String message) => Text( + message, + maxLines: 4, + style: const TextStyle(color: kcWhite, fontWeight: FontWeight.w500), + ); + +void showErrorToast(String message) { + toastification.show( + showIcon: true, + dragToClose: true, + primaryColor: kcRed, + showProgressBar: false, + applyBlurEffect: false, + icon: const Icon(Icons.check), + type: ToastificationType.success, + alignment: Alignment.bottomCenter, + style: ToastificationStyle.fillColored, + description: buildToastDescription(message), + autoCloseDuration: const Duration(seconds: 10), + margin: const EdgeInsets.symmetric(horizontal: 15), + ); +} + +void showSuccessToast(String message) { + toastification.show( + showIcon: true, + dragToClose: true, + showProgressBar: false, + applyBlurEffect: false, + icon: const Icon(Icons.check), + primaryColor: kcPrimaryColor, + type: ToastificationType.success, + alignment: Alignment.bottomCenter, + style: ToastificationStyle.fillColored, + description: buildToastDescription(message), + autoCloseDuration: const Duration(seconds: 10), + margin: const EdgeInsets.symmetric(horizontal: 15), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart b/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart new file mode 100644 index 0000000..faaa377 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/common/validators/form_validator.dart @@ -0,0 +1,64 @@ +import 'package:email_validator/email_validator.dart'; + +class FormValidator { + static String? validateForm(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + return null; + } + + static String? validatePhoneNumber(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + + // Regex validation + final regex = RegExp(r'^251'); + + if (!regex.hasMatch(value)) { + return 'Phone number must start with 251'; + } + + // Length check first (optional but recommended) + if (value.length != 12) { + return 'Phone number must be 12 digits'; + } + return null; + } + + static String? validateEmail(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + + if (!EmailValidator.validate(value)) { + return 'Invalid email format'; + } + + return null; + } + + static String? validatePassword(String? value) { + if (value == null) { + return null; + } + + if (value.isEmpty) { + return 'The field is required'; + } + return null; + } +} diff --git a/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart b/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart deleted file mode 100644 index e0b711e..0000000 --- a/StudioProjects/yimaru_app/lib/ui/common/validators/onboarding_form_validator.dart +++ /dev/null @@ -1,12 +0,0 @@ -class OnboardingFormValidator { - static String? validateForm(String? value) { - if (value == null) { - return null; - } - - if (value.isEmpty) { - return 'The field is required'; - } - return null; - } -} diff --git a/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart new file mode 100644 index 0000000..9599dc7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_view.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/custom_list_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'account_privacy_viewmodel.dart'; + +class AccountPrivacyView extends StackedView { + const AccountPrivacyView({Key? key}) : super(key: key); + + @override + AccountPrivacyViewModel viewModelBuilder( + BuildContext context, + ) => + AccountPrivacyViewModel(); + + @override + Widget builder( + BuildContext context, + AccountPrivacyViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(AccountPrivacyViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(AccountPrivacyViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(AccountPrivacyViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(AccountPrivacyViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(AccountPrivacyViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(AccountPrivacyViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(AccountPrivacyViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(AccountPrivacyViewModel viewModel) => SmallAppBar( + title: 'Account Privacy', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(AccountPrivacyViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(AccountPrivacyViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(AccountPrivacyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(AccountPrivacyViewModel viewModel) => + [_buildMenuColumnScrollView(viewModel), _buildDeleteButtonWrapper()]; + + Widget _buildMenuColumnScrollView(AccountPrivacyViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(AccountPrivacyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(AccountPrivacyViewModel viewModel) => [ + verticalSpaceLarge, + _buildHeader('App Settings'), + verticalSpaceSmall, + _buildLanguageMenu(viewModel), + _buildDividerWrapper(), + verticalSpaceMedium, + _buildHeader('Legal & Information'), + verticalSpaceSmall, + _buildTermsAndConditionsMenu(viewModel), + _buildPrivacyPolicy(viewModel), + _buildDividerWrapper(), + ]; + + Widget _buildHeader(String title) => Text( + title, + style: const TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildLanguageMenu(AccountPrivacyViewModel viewModel) => + CustomListTile( + isLanguage: true, + language: 'English', + icon: Icons.language, + title: 'Change Language', + onTap: () async => await viewModel.navigateToLanguage(), + ); + + Widget _buildTermsAndConditionsMenu(AccountPrivacyViewModel viewModel) => + CustomListTile( + icon: Icons.handshake, + title: 'Terms & Conditions', + onTap: () async => await viewModel.navigateToTerms(), + ); + + Widget _buildPrivacyPolicy(AccountPrivacyViewModel viewModel) => + CustomListTile( + icon: Icons.shield_moon_outlined, + title: 'Privacy Policy', + onTap: () async => await viewModel.navigateToPrivacyPolicy(), + ); + + Widget _buildDividerWrapper() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildDivider(), + ); + + Widget _buildDivider() => const Divider(color: kcVeryLightGrey); + + Widget _buildDeleteButtonWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildDeleteButton(), + ); + Widget _buildDeleteButton() => CustomElevatedButton( + height: 55, + text: 'Delete Account', + borderRadius: 12, + foregroundColor: kcRed, + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart new file mode 100644 index 0000000..6dcc0b7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/account_privacy/account_privacy_viewmodel.dart @@ -0,0 +1,21 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class AccountPrivacyViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); + + Future navigateToLanguage() async => + await _navigationService.navigateToLanguageView(); + + Future navigateToPrivacyPolicy() async => + await _navigationService.navigateToPrivacyPolicyView(); + + Future navigateToTerms() async => + await _navigationService.navigateToTermsAndConditionsView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart new file mode 100644 index 0000000..ccb5e99 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_view.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/circular_icon.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'call_support_viewmodel.dart'; + +class CallSupportView extends StackedView { + const CallSupportView({Key? key}) : super(key: key); + + @override + CallSupportViewModel viewModelBuilder( + BuildContext context, + ) => + CallSupportViewModel(); + + @override + Widget builder( + BuildContext context, + CallSupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(CallSupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(CallSupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(CallSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(CallSupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedColumn(viewModel) + ]; + + Widget _buildAppBarWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(CallSupportViewModel viewModel) => SmallAppBar( + title: 'Call Support', + onTap: viewModel.pop, + ); + + Widget _buildExpandedColumn(CallSupportViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(CallSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(CallSupportViewModel viewModel) => + [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildUpperColumn(CallSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(CallSupportViewModel viewModel) => [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubTitle('+2519012345678'), + verticalSpaceSmall, + _buildSubTitle('+2519012345678'), + ]; + + Widget _buildIcon() => + const CircularIcon(icon: Icons.call, size: 50, color: kcPrimaryColor); + + Widget _buildTitle() => const Text( + 'Call our support team between 9 AM - 6 PM', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle(String title) => Text( + title, + textAlign: TextAlign.center, + style: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(CallSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(CallSupportViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Tap to Call', + leadingIcon: Icons.call, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart new file mode 100644 index 0000000..119e069 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/call_support/call_support_viewmodel.dart @@ -0,0 +1,9 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class CallSupportViewModel extends BaseViewModel { + final _navigationService = locator(); + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart new file mode 100644 index 0000000..27d95ac --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_view.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; +import 'package:yimaru_app/ui/widgets/download_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'downloads_viewmodel.dart'; + +class DownloadsView extends StackedView { + const DownloadsView({Key? key}) : super(key: key); + + @override + DownloadsViewModel viewModelBuilder( + BuildContext context, + ) => + DownloadsViewModel(); + + @override + Widget builder( + BuildContext context, + DownloadsViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(DownloadsViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(DownloadsViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(DownloadsViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(DownloadsViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(DownloadsViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(DownloadsViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppbar(viewModel), + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppbar(DownloadsViewModel viewModel) => SmallAppBar( + title: 'Offline Downloads', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(DownloadsViewModel viewModel) => + viewModel.showDownload + ? _buildEmptyContent(viewModel) + : _buildContent(viewModel); + + Widget _buildContent(DownloadsViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(DownloadsViewModel viewModel) => [ + verticalSpaceMedium, + _buildStorageSection(viewModel), + verticalSpaceLarge, + _buildDownloads(viewModel) + ]; + + Widget _buildStorageSection(DownloadsViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildStorageSectionChildren(viewModel), + ); + + List _buildStorageSectionChildren(DownloadsViewModel viewModel) => [ + _buildStorageInfoWrapper(viewModel), + _buildStorageIndicator(), + ]; + + Widget _buildStorageInfoWrapper(DownloadsViewModel viewModel) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildStorageInfoChildren(viewModel), + ); + + List _buildStorageInfoChildren(DownloadsViewModel viewModel) => + [_buildStorageInfo(), _buildManageButton(viewModel)]; + + Widget _buildStorageInfo() => const Text.rich( + TextSpan( + text: '1.2GB', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: ' used of 2GB', + style: TextStyle( + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); + + Widget _buildManageButton(DownloadsViewModel viewModel) => TextButton( + onPressed: viewModel.setShowDownload, child: _buildManageText()); + + Widget _buildManageText() => const Text( + 'Manage Storage', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildStorageIndicator() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildDownloads(DownloadsViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.downloads.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildDownload( + size: viewModel.downloads[index]['size'], + title: viewModel.downloads[index]['title'], + duration: viewModel.downloads[index]['duration'], + thumbnail: viewModel.downloads[index]['thumbnail']), + ); + + Widget _buildDownload( + {required String title, + required String size, + required String duration, + required String thumbnail}) => + DownloadCard( + size: size, + title: title, + duration: duration, + thumbnail: thumbnail, + ); + + Widget _buildEmptyContent(DownloadsViewModel viewModel) => Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildEmptyContentChildren(viewModel), + ), + ); + + List _buildEmptyContentChildren(DownloadsViewModel viewModel) => + [_buildUpperEmptyContent(viewModel), _buildGoButtonWrapper(viewModel)]; + + Widget _buildUpperEmptyContent(DownloadsViewModel viewModel) => Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildUpperEmptyContentChildren(viewModel), + ), + ); + + List _buildUpperEmptyContentChildren(DownloadsViewModel viewModel) => + [ + verticalSpaceMassive, + _buildEmptyIcon(), + verticalSpaceMedium, + _buildEmptyTitle(), + verticalSpaceSmall, + _buildEmptySubTitle(), + ]; + + Widget _buildEmptyIcon() => const Icon( + Icons.hourglass_empty, + size: 100, + color: kcPrimaryColor, + ); + + Widget _buildEmptyTitle() => const Text( + 'Looking for something to download?', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildEmptySubTitle() => const Text( + 'Start by exploring your learning materials and save them for offline access.', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildGoButtonWrapper(DownloadsViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildGoButton(viewModel), + ); + Widget _buildGoButton(DownloadsViewModel viewModel) => CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Go to Learn Section', + onTap: viewModel.setShowDownload, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart new file mode 100644 index 0000000..0cf971f --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/downloads/downloads_viewmodel.dart @@ -0,0 +1,42 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class DownloadsViewModel extends BaseViewModel { + final _navigationService = locator(); + + bool _showDownload = false; + + bool get showDownload => _showDownload; + // Downloads + final List> _downloads = [ + { + 'size': '120 MB', + 'duration': '3h 46 m', + 'title': 'Duolingo English', + 'thumbnail': 'assets/images/image_1.png', + }, + { + 'size': '79 MB', + 'duration': '1h 34 m', + 'title': 'IELTS Listening', + 'thumbnail': 'assets/images/image_1.png', + }, + { + 'size': '120 MB', + 'duration': '3h 46 m', + 'title': 'Customer Service', + 'thumbnail': 'assets/images/image_1.png', + }, + ]; + + List> get downloads => _downloads; + + void setShowDownload() { + _showDownload = !_showDownload; + rebuildUi(); + } + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart b/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart index 26b32ed..ff5ccb4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/home/home_view.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; -import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/learn/learn_view.dart'; +import 'package:yimaru_app/ui/views/profile/profile_view.dart'; +import 'package:yimaru_app/ui/widgets/coming_soon.dart'; import 'home_viewmodel.dart'; @@ -9,66 +11,61 @@ class HomeView extends StackedView { const HomeView({Key? key}) : super(key: key); @override - Widget builder(BuildContext context, HomeViewModel viewModel, Widget? child) { - return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 25.0), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - verticalSpaceLarge, - Column( - children: [ - const Text( - 'Hello, STACKED!', - style: TextStyle( - fontSize: 35, - fontWeight: FontWeight.w900, - ), - ), - verticalSpaceMedium, - MaterialButton( - color: Colors.black, - onPressed: viewModel.incrementCounter, - child: Text( - viewModel.counterLabel, - style: const TextStyle(color: Colors.white), - ), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MaterialButton( - color: kcDarkGreyColor, - onPressed: viewModel.showDialog, - child: const Text( - 'Show Dialog', - style: TextStyle(color: Colors.white), - ), - ), - MaterialButton( - color: kcDarkGreyColor, - onPressed: viewModel.showBottomSheet, - child: const Text( - 'Show Bottom Sheet', - style: TextStyle(color: Colors.white), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); + HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); + + @override + void onViewModelReady(HomeViewModel viewModel) { + viewModel.getProfileStatus(); + super.onViewModelReady(viewModel); } @override - HomeViewModel viewModelBuilder(BuildContext context) => HomeViewModel(); + Widget builder( + BuildContext context, HomeViewModel viewModel, Widget? child) => + _buildScaffold(viewModel); + + Widget _buildScaffold(HomeViewModel viewModel) => Scaffold( + body: getViewForIndex(viewModel.currentIndex), + bottomNavigationBar: BottomNavigationBar( + onTap: viewModel.setCurrentIndex, + items: _buildNavBarItems(), + selectedItemColor: kcPrimaryColor, + backgroundColor: kcBackgroundColor, + type: BottomNavigationBarType.fixed, + currentIndex: viewModel.currentIndex, + ), + ); + + List _buildNavBarItems() => [ + _buildLearnItem(), + _buildCourseItem(), + _buildProfileItem(), + ]; + + BottomNavigationBarItem _buildLearnItem() => const BottomNavigationBarItem( + label: 'Learn', + icon: Icon(Icons.school), + ); + + BottomNavigationBarItem _buildCourseItem() => const BottomNavigationBarItem( + label: 'Course', + icon: Icon(Icons.book), + ); + + BottomNavigationBarItem _buildProfileItem() => const BottomNavigationBarItem( + label: 'Profile', + icon: Icon(Icons.person), + ); +} + +Widget getViewForIndex(int index) { + switch (index) { + case 0: + return const LearnView(); + case 1: + return const ComingSoon(); + + default: + return const ProfileView(); + } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart index 5928270..428a58f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/home/home_viewmodel.dart @@ -1,20 +1,30 @@ import 'package:yimaru_app/app/app.bottomsheets.dart'; import 'package:yimaru_app/app/app.dialogs.dart'; import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/models/user_model.dart'; import 'package:yimaru_app/ui/common/app_strings.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import '../../../services/api_service.dart'; +import '../../../services/authentication_service.dart'; +import '../../common/enmus.dart'; + class HomeViewModel extends BaseViewModel { + final _apiService = locator(); final _dialogService = locator(); + final _navigationService = locator(); final _bottomSheetService = locator(); + final _authenticationService = locator(); - String get counterLabel => 'Counter is: $_counter'; + // Bottom navigation + int _currentIndex = 0; - int _counter = 0; + int get currentIndex => _currentIndex; - void incrementCounter() { - _counter++; + void setCurrentIndex(int index) { + _currentIndex = index; rebuildUi(); } @@ -22,7 +32,7 @@ class HomeViewModel extends BaseViewModel { _dialogService.showCustomDialog( variant: DialogType.infoAlert, title: 'Stacked Rocks!', - description: 'Give stacked $_counter stars on Github', + description: 'Give stacked stars on Github', ); } @@ -33,4 +43,20 @@ class HomeViewModel extends BaseViewModel { description: ksHomeBottomSheetDescription, ); } + + // Navigation + Future replaceWithOnboarding() async => + await _navigationService.replaceWithOnboardingView(); + + // Remote api calls + Future getProfileStatus() async { + UserModel user = await _authenticationService.getUser(); + + Map response = await runBusyFuture>( + _apiService.getProfileStatus(user)); + + if (response['status'] == ResponseStatus.success && !response['data']) { + await replaceWithOnboarding(); + } + } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart b/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart new file mode 100644 index 0000000..100f9ae --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/language/language_view.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_small_radio_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'language_viewmodel.dart'; + +class LanguageView extends StackedView { + const LanguageView({Key? key}) : super(key: key); + + @override + LanguageViewModel viewModelBuilder( + BuildContext context, + ) => + LanguageViewModel(); + + @override + Widget builder( + BuildContext context, + LanguageViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LanguageViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LanguageViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(LanguageViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LanguageViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedBody(viewModel) + ]; + + Widget _buildExpandedBody(LanguageViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(LanguageViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LanguageViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(LanguageViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + verticalSpaceMedium, + _buildLanguages(viewModel) + ]; + + Widget _buildAppBarWrapper(LanguageViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(LanguageViewModel viewModel) => SmallAppBar( + title: 'Language Preference', + onTap: viewModel.pop, + ); + + Widget _buildTitle() => const Text( + 'Choose your language', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'You can switch languages anytime', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildLanguages(LanguageViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.languages.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildLanguage( + title: viewModel.languages[index]['language'], + selected: viewModel + .isSelectedLanguage(viewModel.languages[index]['language']), + onTap: () => + viewModel.setSelectedLanguage(viewModel.languages[index]), + ), + ); + + Widget _buildLanguage( + {required String title, + required bool selected, + required GestureTapCallback onTap}) => + CustomSmallRadioButton( + title: title, + onTap: onTap, + selected: selected, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart new file mode 100644 index 0000000..74ff8ad --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/language/language_viewmodel.dart @@ -0,0 +1,35 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class LanguageViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Languages + + Map _selectedLanguage = { + 'code': 'EN', + 'language': 'English' + }; + + Map get selectedLanguage => _selectedLanguage; + + final List> _languages = [ + {'code': 'አማ', 'language': 'አማርኛ'}, + {'code': 'EN', 'language': 'English'}, + ]; + + List> get languages => _languages; + + // Languages + void setSelectedLanguage(Map title) { + _selectedLanguage = title; + rebuildUi(); + } + + bool isSelectedLanguage(String title) => + _selectedLanguage['language'] == title; + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart new file mode 100644 index 0000000..b13ab6a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_view.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/learn_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/learn_level_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import 'learn_viewmodel.dart'; + +class LearnView extends StackedView { + const LearnView({Key? key}) : super(key: key); + + @override + LearnViewModel viewModelBuilder(BuildContext context) => LearnViewModel(); + + @override + Widget builder( + BuildContext context, + LearnViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(), + _buildLevelsColumnWrapper(viewModel) + ], + ); + + Widget _buildAppBar() => const LearnAppBar(); + + Widget _buildLevelsColumnWrapper(LearnViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(LearnViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.learnLevels.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.learnLevels[index]['title'], + status: viewModel.learnLevels[index]['status'], + subtitle: viewModel.learnLevels[index]['subtitle']), + ); + + Widget _buildTile( + {required String title, + required String subtitle, + required LearnLevelStatus status}) => + LearnLevelTile( + title: title, + status: status, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart new file mode 100644 index 0000000..0ac3f71 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn/learn_viewmodel.dart @@ -0,0 +1,33 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; + +import '../../../app/app.locator.dart'; + +class LearnViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _learnLevels = [ + { + 'title': 'Beginner', + 'status': LearnLevelStatus.completed, + 'subtitle': 'Start your journey with the basics of English.', + }, + { + 'title': 'Intermediate', + 'status': LearnLevelStatus.started, + 'subtitle': 'Practice real conversations and expand vocabulary.', + }, + { + 'title': 'Advanced', + 'status': LearnLevelStatus.pending, + 'subtitle': 'Achieve fluency and master complex topics.', + }, + ]; + + List> get learnLevels => _learnLevels; + + Future navigateToLearnLevel() async => + _navigationService.navigateToLearnLevelView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart new file mode 100644 index 0000000..7bcfed6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_view.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/learn_sub_level_tile.dart'; +import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import 'learn_level_viewmodel.dart'; + +class LearnLevelView extends StackedView { + const LearnLevelView({Key? key}) : super(key: key); + + @override + LearnLevelViewModel viewModelBuilder(BuildContext context) => + LearnLevelViewModel(); + + @override + Widget builder( + BuildContext context, + LearnLevelViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnLevelViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnLevelViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnLevelViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnLevelViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + _buildLevelsColumnWrapper(viewModel) + ], + ); + + Widget _buildAppBar(LearnLevelViewModel viewModel) => SmallAppBar( + onTap: viewModel.pop, + ); + + Widget _buildLevelsColumnWrapper(LearnLevelViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnLevelViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnLevelViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnLevelViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(LearnLevelViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.learnSubLevels.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.learnSubLevels[index]['title'], + current: viewModel.learnSubLevels[index]['current'], + subtitle: viewModel.learnSubLevels[index]['subtitle']), + ); + + Widget _buildTile({ + required String title, + required bool current, + required String subtitle, + }) => + LearnSubLevelTile( + title: title, + current: current, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart new file mode 100644 index 0000000..01141b6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_level/learn_level_viewmodel.dart @@ -0,0 +1,29 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class LearnLevelViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _learnSubLevels = [ + { + 'title': 'A1', + 'current': true, + 'subtitle': 'Start your journey with the basics of English.', + }, + { + 'title': 'A2', + 'current': false, + 'subtitle': 'Build upon your foundational knowledge.', + }, + ]; + + List> get learnSubLevels => _learnSubLevels; + + Future navigateToLearnModule() async => + _navigationService.navigateToLearnModuleView(); + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart new file mode 100644 index 0000000..db4b79d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_view.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/widgets/learn_module_tile.dart'; +import 'package:yimaru_app/ui/widgets/overall_learn_progress.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'learn_module_viewmodel.dart'; + +class LearnModuleView extends StackedView { + const LearnModuleView({Key? key}) : super(key: key); + + @override + LearnModuleViewModel viewModelBuilder(BuildContext context) => + LearnModuleViewModel(); + + @override + Widget builder( + BuildContext context, + LearnModuleViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(LearnModuleViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(LearnModuleViewModel viewModel) => + SafeArea(child: _buildBody(viewModel)); + + Widget _buildBody(LearnModuleViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(LearnModuleViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildAppBar(viewModel), + _buildLevelsColumnWrapper(viewModel), + ], + ); + + Widget _buildAppBar(LearnModuleViewModel viewModel) => SmallAppBar( + onTap: viewModel.pop, + ); + + Widget _buildLevelsColumnWrapper(LearnModuleViewModel viewModel) => + Expanded(child: _buildLevelsColumnScrollView(viewModel)); + + Widget _buildLevelsColumnScrollView(LearnModuleViewModel viewModel) => + SingleChildScrollView( + child: _buildLevelsColumn(viewModel), + ); + + Widget _buildLevelsColumn(LearnModuleViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLevelsColumnChildren(viewModel), + ); + + List _buildLevelsColumnChildren(LearnModuleViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitle(), + verticalSpaceMedium, + _buildOverallProgress(), + verticalSpaceMedium, + _buildListView(viewModel) + ]; + + Widget _buildTitle() => const Text( + 'A1 - Beginner', + style: TextStyle( + fontSize: 18, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'Your Current Level', + style: TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildOverallProgress() => const OverallLearnProgress(); + + Widget _buildListView(LearnModuleViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.modules.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildTile( + title: viewModel.modules[index]['title'], + status: viewModel.modules[index]['status'], + subtitle: viewModel.modules[index]['subtitle']), + ); + + Widget _buildTile({ + required String title, + required String subtitle, + required LearnLevelStatus status, + }) => + LearnModuleTile( + title: title, + status: status, + subtitle: subtitle, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart new file mode 100644 index 0000000..87eaa35 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/learn_module/learn_module_viewmodel.dart @@ -0,0 +1,38 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; +import '../../common/enmus.dart'; + +class LearnModuleViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _modules = [ + { + 'status': LearnLevelStatus.started, + 'title': 'Module 1: Greetings & Introductions', + 'subtitle': + 'Learn how to introduce yourself, talk about your surroundings, and start simple conversations.', + }, + { + 'status': LearnLevelStatus.pending, + 'title': 'Module 2: Everyday Basics', + 'subtitle': 'Learn numbers, colors, and common objects.', + }, + { + 'title': 'Module 3: At the Cafe', + 'status': LearnLevelStatus.pending, + 'subtitle': 'Practice ordering food and drinks confidently.', + }, + { + 'progress': 0, + 'status': LearnLevelStatus.pending, + 'title': 'Module 4: Asking for Directions', + 'subtitle': 'Learn numbers, colors, and common objects.', + }, + ]; + + List> get modules => _modules; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart new file mode 100644 index 0000000..068a0f4 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_otp_screen.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_with_email_screen.dart'; +import 'package:yimaru_app/ui/views/login/screens/login_with_phone_number_screen.dart'; + +import '../../common/app_colors.dart'; +import '../../common/validators/form_validator.dart'; +import '../../widgets/large_app_bar.dart'; +import '../../widgets/page_loading_indicator.dart'; +import 'login_viewmodel.dart'; + +import 'login_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'otp', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmail), + FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm) +]) +class LoginView extends StackedView with $LoginView { + const LoginView({Key? key}) : super(key: key); + + @override + void onViewModelReady(LoginViewModel viewModel) { + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + LoginViewModel viewModelBuilder(BuildContext context) => LoginViewModel(); + + @override + Widget builder( + BuildContext context, + LoginViewModel viewModel, + Widget? child, + ) => + _buildLoginScreensWrapper(viewModel); + + Widget _buildLoginScreensWrapper(LoginViewModel viewModel) => PopScope( + canPop: true, + onPopInvokedWithResult: (value, data) { + if (!value) return; + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + }, + child: _buildScaffoldWrapper(viewModel)); + + Widget _buildScaffoldWrapper(LoginViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffoldStack(viewModel), + ); + + Widget _buildScaffoldStack(LoginViewModel viewModel) => + Stack(children: [_buildScaffold(viewModel), _buildBusyLogin(viewModel)]); + + Widget _buildScaffold(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildScaffoldChildren(viewModel), + ); + + List _buildScaffoldChildren(LoginViewModel viewModel) => + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; + + Widget _buildAppBar(LoginViewModel viewModel) => const LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + ); + Widget _buildExpandedBody(LoginViewModel viewModel) => + Expanded(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(LoginViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(LoginViewModel viewModel) => + IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); + + List _buildScreens() => [ + _buildLoginWithEmailScreen(), + _buildLoginWithPhoneScreen(), + _buildLoginOtpScreen() + ]; + + Widget _buildLoginWithEmailScreen() => LoginWithEmailScreen( + emailController: emailController, passwordController: passwordController); + + Widget _buildLoginWithPhoneScreen() => + LoginWithPhoneNumberScreen(phoneNumberController: phoneNumberController); + + Widget _buildLoginOtpScreen() => LoginOtpScreen( + otpController: otpController, + phoneNumberController: phoneNumberController); + + Widget _buildBusyLogin(LoginViewModel viewModel) => + viewModel.isBusy ? const PageLoadingIndicator() : Container(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart new file mode 100644 index 0000000..5a64de2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_view.form.dart @@ -0,0 +1,269 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String OtpValueKey = 'otp'; +const String EmailValueKey = 'email'; +const String PasswordValueKey = 'password'; +const String PhoneNumberValueKey = 'phoneNumber'; + +final Map _LoginViewTextEditingControllers = {}; + +final Map _LoginViewFocusNodes = {}; + +final Map _LoginViewTextValidations = { + OtpValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmail, + PasswordValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validateForm, +}; + +mixin $LoginView { + TextEditingController get otpController => + _getFormTextEditingController(OtpValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get passwordController => + _getFormTextEditingController(PasswordValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + + FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_LoginViewTextEditingControllers.containsKey(key)) { + return _LoginViewTextEditingControllers[key]!; + } + + _LoginViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _LoginViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_LoginViewFocusNodes.containsKey(key)) { + return _LoginViewFocusNodes[key]!; + } + _LoginViewFocusNodes[key] = FocusNode(); + return _LoginViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + OtpValueKey: otpController.text, + EmailValueKey: emailController.text, + PasswordValueKey: passwordController.text, + PhoneNumberValueKey: phoneNumberController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _LoginViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _LoginViewFocusNodes.values) { + focusNode.dispose(); + } + + _LoginViewTextEditingControllers.clear(); + _LoginViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get otpValue => this.formValueMap[OtpValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + + set otpValue(String? value) { + this.setData( + this.formValueMap..addAll({OtpValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(OtpValueKey)) { + _LoginViewTextEditingControllers[OtpValueKey]?.text = value ?? ''; + } + } + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(EmailValueKey)) { + _LoginViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; + } + } + + set passwordValue(String? value) { + this.setData( + this.formValueMap..addAll({PasswordValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(PasswordValueKey)) { + _LoginViewTextEditingControllers[PasswordValueKey]?.text = value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_LoginViewTextEditingControllers.containsKey(PhoneNumberValueKey)) { + _LoginViewTextEditingControllers[PhoneNumberValueKey]?.text = value ?? ''; + } + } + + bool get hasOtp => + this.formValueMap.containsKey(OtpValueKey) && + (otpValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPassword => + this.formValueMap.containsKey(PasswordValueKey) && + (passwordValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + + bool get hasOtpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPasswordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + + String? get otpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get passwordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; +} + +extension Methods on FormStateHelper { + setOtpValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[OtpValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + otpValue = ''; + emailValue = ''; + passwordValue = ''; + phoneNumberValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _LoginViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _LoginViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart new file mode 100644 index 0000000..a58582d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/login_viewmodel.dart @@ -0,0 +1,161 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.locator.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/models/user_model.dart'; + +import '../../../services/api_service.dart'; +import '../../../services/authentication_service.dart'; +import '../../common/enmus.dart'; +import '../../common/ui_helpers.dart'; +import '../home/home_view.dart'; + +class LoginViewModel extends FormViewModel { + final _apiService = locator(); + final _navigationService = locator(); + final _authenticationService = locator(); + + // Navigation + int _currentIndex = 0; + + int get currentIndex => _currentIndex; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // Password + bool _focusPassword = false; + + bool get focusPassword => _focusPassword; + + bool _obscurePassword = true; + + bool get obscurePassword => _obscurePassword; + + // Phone number + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Focus otp + bool _focusOtp = false; + + bool get focusOtp => _focusOtp; + + // Focus node + final FocusNode _focusNode = FocusNode(); + + FocusNode get focusNode => _focusNode; + + // Resend button state + bool _buttonActive = false; + + bool get buttonActive => _buttonActive; + + // User data + final Map _userData = {}; + + Map get userData => _userData; + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Password + void setPasswordFocus() { + _focusPassword = true; + rebuildUi(); + } + + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Otp + void setOtpFocus() { + _focusOtp = true; + rebuildUi(); + } + + // Validate otp + Future validateOtp(String otp) async {} + + void setResendButton() { + _buttonActive = true; + rebuildUi(); + } + + // Add user data + void addUserData(Map data) { + _userData.addAll(data); + } + + void clearUserData() { + _userData.clear(); + } + + // Remote api calls + Future login() async { + Map response = + await runBusyFuture>(_login()); + + if (response['status'] == ResponseStatus.success) { + await replaceWithHome(); + } + } + + Future> _login() async { + Map response = await _apiService.login(_userData); + if (response['status'] == ResponseStatus.success) { + UserModel user = response['data'] as UserModel; + Map data = { + 'userId': user.userId, + 'accessToken': user.accessToken, + 'refreshToken': user.refreshToken + }; + + await _authenticationService.saveUserData(data); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + + return response; + } + + // Navigation + void goTo(int page) { + _currentIndex = page; + rebuildUi(); + } + + void goBack() { + if (_currentIndex == 1) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 2) { + _currentIndex = 1; + rebuildUi(); + } else { + _navigationService.back(); + } + } + + Future navigateToRegister() async => + await _navigationService.navigateToRegisterView(); + + Future replaceWithHome() async => + await _navigationService.clearStackAndShowView(const HomeView()); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart new file mode 100644 index 0000000..cd8d881 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_otp_screen.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timer_countdown/flutter_timer_countdown.dart'; +import 'package:pinput/pinput.dart'; +import 'package:stacked/stacked.dart'; + +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_cursor.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/large_app_bar.dart'; +import '../login_viewmodel.dart'; +import '../login_view.form.dart'; + +class LoginOtpScreen extends ViewModelWidget { + final TextEditingController otpController; + final TextEditingController phoneNumberController; + + const LoginOtpScreen( + {super.key, + required this.otpController, + required this.phoneNumberController}); + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubtitleWrapper(), + verticalSpaceMedium, + _buildPinPutWrapper(viewModel), + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + verticalSpaceTiny, + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + _buildOtpValidatorWrapper(viewModel), + verticalSpaceSmall, + _buildTimerWrapper(viewModel) + ]; + + Widget _buildTitle() => Text( + 'Verification Code', + style: style25DG600, + ); + + Widget _buildSubtitleWrapper() => + phoneNumberController.text.length == 9 ? _buildSubtitle() : Container(); + + Widget _buildSubtitle() => Text( + 'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****', + style: style14DG400, + ); + + Widget _buildPinPutWrapper(LoginViewModel viewModel) => Center( + child: _buildPinPut(viewModel), + ); + + Widget _buildPinPut(LoginViewModel viewModel) => Pinput( + controller: otpController, + defaultPinTheme: defaultPin, + cursor: const CustomCursor(), + errorPinTheme: errorPinTheme, + onTap: viewModel.setOtpFocus, + focusNode: viewModel.focusNode, + errorTextStyle: validationStyle, + //smsRetriever: locator(), + focusedPinTheme: focusedThemePin, + submittedPinTheme: submittedThemePin, + hapticFeedbackType: HapticFeedbackType.heavyImpact, + separatorBuilder: (index) => const SizedBox(width: 25), + onCompleted: (otp) async => await viewModel.validateOtp(otp), + ); + + Widget _buildOtpValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasOtpValidationMessage + ? _buildOtpValidator(viewModel) + : Container(); + + Widget _buildOtpValidator(LoginViewModel viewModel) => Text( + viewModel.otpValidationMessage!, + style: style12R700, + ); + + Widget _buildTimerWrapper(LoginViewModel viewModel) => !viewModel.buttonActive + ? _buildTimerSection(viewModel) + : _buildResendButton(); + + Widget _buildResendButton() => + TextButton(onPressed: () {}, child: _buildResendText()); + + Widget _buildResendText() => Text( + 'Resend code', + style: style14P600.copyWith(fontStyle: FontStyle.italic), + ); + + Widget _buildTimerSection(LoginViewModel viewModel) => Row( + children: [ + _buildCountdownText(), + horizontalSpaceSmall, + _buildTimer(viewModel) + ], + ); + + Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400); + + Widget _buildTimer(LoginViewModel viewModel) => TimerCountdown( + enableDescriptions: false, + timeTextStyle: style14P600, + endTime: DateTime.now().add(const Duration(minutes: 3, seconds: 0)), + onEnd: viewModel.setResendButton, + format: CountDownTimerFormat.minutesSeconds, + colonsTextStyle: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(LoginViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: viewModel.focusOtp && + otpController.text.length == 4 && + !viewModel.hasOtpValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: viewModel.focusOtp && + otpController.text.length == 4 && + !viewModel.hasOtpValidationMessage + ? () => viewModel.replaceWithHome() + : null, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart new file mode 100644 index 0000000..5e31632 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_email_screen.dart @@ -0,0 +1,201 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/login/login_view.form.dart'; +import 'package:yimaru_app/ui/widgets/obscure_password.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/option_text_divider.dart'; +import '../../../widgets/register_for_account.dart'; +import '../login_viewmodel.dart'; + +class LoginWithEmailScreen extends ViewModelWidget { + final TextEditingController emailController; + final TextEditingController passwordController; + + const LoginWithEmailScreen( + {super.key, + required this.emailController, + required this.passwordController}); + + Future _login(LoginViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'email': emailController.text, + 'password': passwordController.text, + }; + viewModel.addUserData(data); + + await viewModel.login(); + } + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceLarge, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel), + verticalSpaceMedium, + _buildPasswordFormField(viewModel), + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + verticalSpaceTiny, + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + _buildPasswordValidationWrapper(viewModel), + _buildForgetPasswordTextButtonWrapper(), + ]; + + Widget _buildTitle() => const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + onTap: () async => await viewModel.navigateToRegister(), + ); + + Widget _buildEmailFormField(LoginViewModel viewModel) => TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + onTap: viewModel.setEmailFocus, + decoration: inputDecoration( + hint: 'Email', + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(LoginViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildPasswordFormField(LoginViewModel viewModel) => TextFormField( + controller: passwordController, + onTap: viewModel.setPasswordFocus, + obscureText: viewModel.obscurePassword, + decoration: inputDecoration( + hint: 'Password', + focus: viewModel.focusPassword, + suffix: _buildObscureButton(viewModel), + filled: passwordController.text.isNotEmpty), + ); + + Widget _buildObscureButton(LoginViewModel viewModel) => ObscurePassword( + focus: viewModel.focusPassword, + obscure: viewModel.obscurePassword, + onTap: viewModel.setObscurePassword, + ); + + Widget _buildPasswordValidationWrapper(LoginViewModel viewModel) => + viewModel.hasPasswordValidationMessage + ? _buildPasswordValidator(viewModel) + : Container(); + + Widget _buildPasswordValidator(LoginViewModel viewModel) => Text( + viewModel.passwordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildForgetPasswordTextButtonWrapper() => Align( + alignment: Alignment.centerRight, + child: _buildForgetPasswordTextButton(), + ); + + Widget _buildForgetPasswordTextButton() => TextButton( + onPressed: () {}, + child: _buildForgetPasswordText(), + ); + + Widget _buildForgetPasswordText() => const Text( + 'Forget Password?', + style: TextStyle(color: kcPrimaryColor), + ); + + Widget _buildLowerColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(LoginViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildLoginWithEmailButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: (viewModel.focusEmail && + emailController.text.isNotEmpty) && + (viewModel.focusPassword && passwordController.text.isNotEmpty) + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: (viewModel.focusEmail && emailController.text.isNotEmpty) && + (viewModel.focusPassword && passwordController.text.isNotEmpty) + ? () async => await _login(viewModel) + : null, + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildLoginWithEmailButton(LoginViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.phone, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: 'Login with Phone Number', + onTap: () => viewModel.goTo(1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart new file mode 100644 index 0000000..b42e4cf --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/login/screens/login_with_phone_number_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/login/login_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/option_text_divider.dart'; +import 'package:yimaru_app/ui/widgets/register_for_account.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/phone_number_prefix.dart'; +import '../login_view.form.dart'; + +class LoginWithPhoneNumberScreen extends ViewModelWidget { + final TextEditingController phoneNumberController; + const LoginWithPhoneNumberScreen( + {super.key, required this.phoneNumberController}); + + @override + Widget build(BuildContext context, LoginViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(LoginViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(LoginViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(LoginViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(LoginViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceMedium, + _buildSubtitle(), + verticalSpaceMedium, + _buildPhoneNumberWrapper(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Welcome Back', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(LoginViewModel viewModel) => RegisterForAccount( + onTap: () async => await viewModel.navigateToRegister(), + ); + Widget _buildSubtitle() => const Text( + 'Enter your phone number. We will send you a confirmation code there', + style: TextStyle(color: kcMediumGrey), + ); + Widget _buildPhoneNumberWrapper(LoginViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberChildren(viewModel), + ); + + List _buildPhoneNumberChildren(LoginViewModel viewModel) => [ + _buildPhoneNumberPrefix(viewModel), + horizontalSpaceSmall, + _buildPhoneNumberFormFieldWrapper(viewModel), + ]; + + Widget _buildPhoneNumberPrefix(LoginViewModel viewModel) => + PhoneNumberPrefix(selected: viewModel.focusPhoneNumber); + + Widget _buildPhoneNumberFormFieldWrapper(LoginViewModel viewModel) => + Expanded(child: _buildPhoneNumberFormField(viewModel)); + + Widget _buildPhoneNumberFormField(LoginViewModel viewModel) => TextFormField( + maxLength: 9, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(LoginViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(LoginViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(LoginViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(LoginViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildLoginWitPhoneNumberButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildContinueButton(LoginViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? () => viewModel.goTo(2) + : null, + backgroundColor: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildLoginWitPhoneNumberButton(LoginViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Login with Email', + backgroundColor: kcWhite, + leadingIcon: Icons.email, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + onTap: () => viewModel.goTo(0), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart index 4206bcc..bddc8a8 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.dart @@ -1,52 +1,64 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked/stacked_annotations.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/assessment_result_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/result_analysis_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/start_lesson_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/age_group_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/challenge_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/country_region_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/educational_background_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/full_name_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/occupation_form_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/forms/topic_form_screen.dart'; import 'package:yimaru_app/ui/views/onboarding/screens/language_selector.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome.dart'; -import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/first_welcome_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/second_welcome_screen.dart'; +import 'package:yimaru_app/ui/views/onboarding/screens/welcome/third_welcome_screen.dart'; -import '../../common/validators/onboarding_form_validator.dart'; +import '../../common/validators/form_validator.dart'; import 'onboarding_viewmodel.dart'; import 'onboarding_view.form.dart'; @FormView(fields: [ - FormTextField( - name: 'answer', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'fullName', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'challenge', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'occupation', validator: OnboardingFormValidator.validateForm), - FormTextField( - name: 'learningReason', validator: OnboardingFormValidator.validateForm), - FormTextField(name: 'topic', validator: OnboardingFormValidator.validateForm), + FormTextField(name: 'answer', validator: FormValidator.validateForm), + FormTextField(name: 'fullName', validator: FormValidator.validateForm), + FormTextField(name: 'challenge', validator: FormValidator.validateForm), + FormTextField(name: 'occupation', validator: FormValidator.validateForm), + FormTextField(name: 'learningReason', validator: FormValidator.validateForm), + FormTextField(name: 'topic', validator: FormValidator.validateForm), ]) class OnboardingView extends StackedView with $OnboardingView { const OnboardingView({Key? key}) : super(key: key); + void _initFormFields() { + answerController.text = 'Book'; + } + + @override + void onViewModelReady(OnboardingViewModel viewModel) { + _initFormFields(); + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + OnboardingViewModel viewModelBuilder( + BuildContext context, + ) => + OnboardingViewModel(); + @override Widget builder( BuildContext context, @@ -94,68 +106,57 @@ class OnboardingView extends StackedView _buildLanguageSelector() ]; - Widget _buildFirstWelcome() => const FirstWelcome(); + Widget _buildFirstWelcome() => const FirstWelcomeScreen(); - Widget _buildSecondWelcome() => const SecondWelcome(); + Widget _buildSecondWelcome() => const SecondWelcomeScreen(); - Widget _buildThirdWelcome() => const ThirdWelcome(); + Widget _buildThirdWelcome() => const ThirdWelcomeScreen(); Widget _buildFullNameForm() => - FullNameForm(fullNameController: fullNameController); + FullNameFormScreen(fullNameController: fullNameController); - Widget _buildEducationalBackgroundForm() => const EducationalBackgroundForm(); + Widget _buildEducationalBackgroundForm() => + const EducationalBackgroundFormScreen(); - Widget _buildAgeGroupForm() => const AgeGroupForm(); + Widget _buildAgeGroupForm() => const AgeGroupFormScreen(); Widget _buildOccupationForm() => - OccupationForm(occupationController: occupationController); + OccupationFormScreen(occupationController: occupationController); - Widget _buildCountryRegionForm() => const CountryRegionForm(); + Widget _buildCountryRegionForm() => const CountryRegionFormScreen(); - Widget _buildLearningGoalForm() => const LearningGoalForm(); + Widget _buildLearningGoalForm() => const LearningGoalFormScreen(); - Widget _buildLearningReasonForm() => - LearningReasonForm(learningReasonController: learningReasonController); + Widget _buildLearningReasonForm() => LearningReasonFormScreen( + learningReasonController: learningReasonController); Widget _buildChallengeForm() => - ChallengeForm(challengeController: challengeController); + ChallengeFormScreen(challengeController: challengeController); - Widget _buildTopicForm() => TopicForm(topicController: topicController); + Widget _buildTopicForm() => TopicFormScreen(topicController: topicController); - Widget _buildAssessmentIntro() => const AssessmentIntro(); + Widget _buildAssessmentIntro() => const AssessmentIntroScreen(); Widget _buildFirstAssessmentForm() => - FirstAssessmentForm(answerController: answerController); + FirstAssessmentFormScreen(answerController: answerController); - Widget _buildSecondAssessment() => const SecondAssessmentForm(); + Widget _buildSecondAssessment() => const SecondAssessmentFormScreen(); - Widget _buildThirdAssessment() => const ThirdAssessmentForm(); + Widget _buildThirdAssessment() => const ThirdAssessmentFormScreen(); - Widget _buildFourthAssessment() => const FourthAssessmentForm(); + Widget _buildFourthAssessment() => const FourthAssessmentFormScreen(); - Widget _buildAssessmentFailure() => const AssessmentFailure(); + Widget _buildAssessmentFailure() => const AssessmentFailureScreen(); - Widget _buildRetakeAssessment() => const RetakeAssessment(); + Widget _buildRetakeAssessment() => const RetakeAssessmentScreen(); - Widget _buildResultAnalysis() => const ResultAnalysis(); + Widget _buildResultAnalysis() => const ResultAnalysisScreen(); - Widget _buildAssessmentCompletion() => const AssessmentCompletion(); + Widget _buildAssessmentCompletion() => const AssessmentCompletionScreen(); - Widget _buildAssessmentResult() => const AssessmentResult(); + Widget _buildAssessmentResult() => const AssessmentResultScreen(); - Widget _buildStartLesson() => const StartLesson(); + Widget _buildStartLesson() => const StartLessonScreen(); Widget _buildLanguageSelector() => const LanguageSelector(); - - @override - void onViewModelReady(OnboardingViewModel viewModel) { - syncFormWithViewModel(viewModel); - super.onViewModelReady(viewModel); - } - - @override - OnboardingViewModel viewModelBuilder( - BuildContext context, - ) => - OnboardingViewModel(); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart index c2c4d89..2aed793 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_view.form.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/common/validators/onboarding_form_validator.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; const bool _autoTextFieldValidation = true; @@ -25,12 +25,12 @@ final Map _OnboardingViewTextEditingControllers = final Map _OnboardingViewFocusNodes = {}; final Map _OnboardingViewTextValidations = { - AnswerValueKey: OnboardingFormValidator.validateForm, - FullNameValueKey: OnboardingFormValidator.validateForm, - ChallengeValueKey: OnboardingFormValidator.validateForm, - OccupationValueKey: OnboardingFormValidator.validateForm, - LearningReasonValueKey: OnboardingFormValidator.validateForm, - TopicValueKey: OnboardingFormValidator.validateForm, + AnswerValueKey: FormValidator.validateForm, + FullNameValueKey: FormValidator.validateForm, + ChallengeValueKey: FormValidator.validateForm, + OccupationValueKey: FormValidator.validateForm, + LearningReasonValueKey: FormValidator.validateForm, + TopicValueKey: FormValidator.validateForm, }; mixin $OnboardingView { diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart index 2cf1fef..e74175d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/onboarding_viewmodel.dart @@ -2,7 +2,6 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:yimaru_app/app/app.router.dart'; import '../../../app/app.locator.dart'; -import 'onboarding_view.form.dart'; class OnboardingViewModel extends FormViewModel { final _navigationService = locator(); @@ -26,7 +25,7 @@ class OnboardingViewModel extends FormViewModel { 'Primary school', 'Secondary /High school', 'College / Diploma', - 'Bachelor’s and above' + 'Bachelor’s and above', ]; List get educationalBackgrounds => _educationalBackgrounds; @@ -357,6 +356,10 @@ class OnboardingViewModel extends FormViewModel { bool isSelectedLanguage(String title) => _selectedLanguage['language'] == title; + // Navigation + Future navigateToHome() async => + await _navigationService.navigateToHomeView(); + void next({int? page}) async { if (page == null) { if (_previousPage != 0) { diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart index 1b7641a..5cd813a 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_completion_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentCompletion extends ViewModelWidget { - const AssessmentCompletion({super.key}); +class AssessmentCompletionScreen extends ViewModelWidget { + const AssessmentCompletionScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -27,7 +27,7 @@ class AssessmentCompletion extends ViewModelWidget { List _buildScaffoldChildren(OnboardingViewModel viewModel) => [_buildAppBar(), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -73,7 +73,7 @@ class AssessmentCompletion extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -95,7 +95,7 @@ class AssessmentCompletion extends ViewModelWidget { borderRadius: 12, text: 'View My Results', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart similarity index 85% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart index c2a5fc6..d65cdfb 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_failure_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentFailure extends ViewModelWidget { - const AssessmentFailure({super.key}); +class AssessmentFailureScreen extends ViewModelWidget { + const AssessmentFailureScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,9 +25,15 @@ class AssessmentFailure extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -69,7 +75,7 @@ class AssessmentFailure extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -87,17 +93,18 @@ class AssessmentFailure extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, borderRadius: 12, text: 'Continue Assessment', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -113,7 +120,7 @@ class AssessmentFailure extends ViewModelWidget { borderRadius: 12, borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart similarity index 84% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart index fc376a0..150de8d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_intro_screen.dart @@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentIntro extends ViewModelWidget { - const AssessmentIntro({super.key}); +class AssessmentIntroScreen extends ViewModelWidget { + const AssessmentIntroScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -24,7 +24,7 @@ class AssessmentIntro extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -56,13 +56,19 @@ class AssessmentIntro extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Want a quick assessment to know your English level?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -79,17 +85,18 @@ class AssessmentIntro extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -104,7 +111,7 @@ class AssessmentIntro extends ViewModelWidget { text: 'Skip', borderRadius: 12, borderColor: kcPrimaryColor, - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart index c7e2186..7b3350b 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/assessment_result_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AssessmentResult extends ViewModelWidget { - const AssessmentResult({super.key}); +class AssessmentResultScreen extends ViewModelWidget { + const AssessmentResultScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,9 +25,15 @@ class AssessmentResult extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -95,17 +101,18 @@ class AssessmentResult extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -121,7 +128,7 @@ class AssessmentResult extends ViewModelWidget { text: 'Practice Speaking', borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart index 5ccb94e..667e921 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/first_assessment_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class FirstAssessmentForm extends ViewModelWidget { +class FirstAssessmentFormScreen extends ViewModelWidget { final TextEditingController answerController; - const FirstAssessmentForm({super.key, required this.answerController}); + const FirstAssessmentFormScreen({super.key, required this.answerController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -66,7 +66,7 @@ class FirstAssessmentForm extends ViewModelWidget { _buildFirstAssessmentValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -75,7 +75,7 @@ class FirstAssessmentForm extends ViewModelWidget { '1. What is the plural of “book”?', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -84,7 +84,9 @@ class FirstAssessmentForm extends ViewModelWidget { TextFormField( controller: answerController, onTap: viewModel.setFirstAssessmentFocus, - decoration: inputDecoration(focus: viewModel.focusFirstAssessment), + decoration: inputDecoration( + focus: viewModel.focusFirstAssessment, + filled: answerController.text.isNotEmpty), ); Widget _buildFirstAssessmentValidatorWrapper(OnboardingViewModel viewModel) => @@ -111,13 +113,15 @@ class FirstAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, - backgroundColor: - viewModel.focusFirstAssessment && answerController.text.isNotEmpty + foregroundColor: kcWhite, + backgroundColor: answerController.text.isNotEmpty + ? kcPrimaryColor + : viewModel.focusFirstAssessment && answerController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), - onTap: - viewModel.focusFirstAssessment && answerController.text.isNotEmpty + : kcPrimaryColor.withOpacity(0.1), + onTap: answerController.text.isNotEmpty + ? () => viewModel.next() + : viewModel.focusFirstAssessment && answerController.text.isNotEmpty ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart index 5fb4f43..2f8d446 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/fourth_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class FourthAssessmentForm extends ViewModelWidget { - const FourthAssessmentForm({super.key}); +class FourthAssessmentFormScreen extends ViewModelWidget { + const FourthAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class FourthAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class FourthAssessmentForm extends ViewModelWidget { 'Q4.  Choose the word that best matches the meaning of ‘meticulous’:', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -104,11 +104,11 @@ class FourthAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedA4Answer != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedA4Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart index 48399e5..a7dafee 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/result_analysis_screen.dart @@ -3,12 +3,11 @@ import 'package:flutter_svg/svg.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; -import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ResultAnalysis extends ViewModelWidget { - const ResultAnalysis({super.key}); +class ResultAnalysisScreen extends ViewModelWidget { + const ResultAnalysisScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,7 +24,7 @@ class ResultAnalysis extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -51,7 +50,11 @@ class ResultAnalysis extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildIcon() => SvgPicture.asset( 'assets/icons/progress_indicator.svg', @@ -62,7 +65,7 @@ class ResultAnalysis extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart similarity index 85% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart index f960b11..f552473 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/retake_assessment_screen.dart @@ -4,10 +4,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class RetakeAssessment extends ViewModelWidget { - const RetakeAssessment({super.key}); +class RetakeAssessmentScreen extends ViewModelWidget { + const RetakeAssessmentScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -24,7 +24,7 @@ class RetakeAssessment extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -59,7 +59,11 @@ class RetakeAssessment extends ViewModelWidget { _buildSubTitle(), ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildIcon() => const Icon( Icons.warning_amber_rounded, @@ -72,7 +76,7 @@ class RetakeAssessment extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -90,17 +94,18 @@ class RetakeAssessment extends ViewModelWidget { List _buildLowerColumnChildren(OnboardingViewModel viewModel) => [ _buildContinueButton(viewModel), - verticalSpaceMedium, + verticalSpaceSmall, _buildSkipButtonWrapper(viewModel) ]; Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, + safe: false, borderRadius: 12, text: 'Retake Assessment', onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); @@ -116,7 +121,7 @@ class RetakeAssessment extends ViewModelWidget { borderRadius: 12, borderColor: kcPrimaryColor, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, + backgroundColor: kcWhite, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart similarity index 88% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart index 0869e2c..7f0a973 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/second_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class SecondAssessmentForm extends ViewModelWidget { - const SecondAssessmentForm({super.key}); +class SecondAssessmentFormScreen extends ViewModelWidget { + const SecondAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class SecondAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class SecondAssessmentForm extends ViewModelWidget { 'Q2. Choose the correct word to complete the sentence:\nI ____ to school yesterday. ', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -103,12 +103,11 @@ class SecondAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, - onTap: viewModel.selectedA2Answer != null - ? () => viewModel.next() - : null, + foregroundColor: kcWhite, + onTap: + viewModel.selectedA2Answer != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedA2Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart similarity index 89% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart index d8de773..510cac4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/start_lesson_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class StartLesson extends ViewModelWidget { - const StartLesson({super.key}); +class StartLessonScreen extends ViewModelWidget { + const StartLessonScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -27,7 +27,7 @@ class StartLesson extends ViewModelWidget { List _buildScaffoldChildren(OnboardingViewModel viewModel) => [_buildAppBar(), _buildExpandedBody(viewModel)]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -71,7 +71,7 @@ class StartLesson extends ViewModelWidget { text: 'Welcome aboard', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), children: [ @@ -97,11 +97,12 @@ class StartLesson extends ViewModelWidget { ); Widget _buildContinueButton(OnboardingViewModel viewModel) => - const CustomElevatedButton( + CustomElevatedButton( height: 55, borderRadius: 12, text: 'Go to My Lessons', - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, + onTap: () async => await viewModel.navigateToHome(), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart similarity index 91% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart index 2c7cd26..609cc80 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/assessment/third_assessment_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ThirdAssessmentForm extends ViewModelWidget { - const ThirdAssessmentForm({super.key}); +class ThirdAssessmentFormScreen extends ViewModelWidget { + const ThirdAssessmentFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,7 +57,7 @@ class ThirdAssessmentForm extends ViewModelWidget { _buildAnswers(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar( + Widget _buildAppBar() => const LargeAppBar( showBackButton: false, showLanguageSelection: false, ); @@ -66,7 +66,7 @@ class ThirdAssessmentForm extends ViewModelWidget { 'Q3. Which word means the same as ‘expand’?', style: TextStyle( fontSize: 16, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -103,10 +103,10 @@ class ThirdAssessmentForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: viewModel.selectedA3Answer != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), onTap: viewModel.selectedA3Answer != null ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart index b8d0f9c..d6f8c3f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/age_group_form_screen.dart @@ -5,17 +5,15 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class AgeGroupForm extends ViewModelWidget { - const AgeGroupForm({super.key}); +class AgeGroupFormScreen extends ViewModelWidget { + const AgeGroupFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -27,11 +25,15 @@ class AgeGroupForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildColumnScroller(viewModel)); + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: _buildBody(viewModel), @@ -61,13 +63,20 @@ class AgeGroupForm extends ViewModelWidget { _buildAgeGroups(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Which age range are you in?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -109,10 +118,10 @@ class AgeGroupForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: viewModel.selectedAgeGroup != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), onTap: viewModel.selectedAgeGroup != null ? () => viewModel.next() : null, ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart index 8eee3aa..11870a8 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/challenge_form_screen.dart @@ -6,12 +6,12 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class ChallengeForm extends ViewModelWidget { +class ChallengeFormScreen extends ViewModelWidget { final TextEditingController challengeController; - const ChallengeForm({super.key, required this.challengeController}); + const ChallengeFormScreen({super.key, required this.challengeController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,17 +28,18 @@ class ChallengeForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -75,13 +76,20 @@ class ChallengeForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What challenge do you face most with English?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -119,7 +127,10 @@ class ChallengeForm extends ViewModelWidget { maxLines: 3, controller: challengeController, onTap: viewModel.setChallengesFocus, - decoration: inputDecoration(focus: true, hint: 'Write your challenge…'), + decoration: inputDecoration( + focus: true, + hint: 'Write your challenge…', + filled: challengeController.text.isNotEmpty), ); Widget _buildChallengeValidatorWrapper(OnboardingViewModel viewModel) => @@ -146,7 +157,7 @@ class ChallengeForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedChallenge != null ? viewModel.selectedChallenge?.toLowerCase() == 'other' ? viewModel.focusChallenge @@ -159,7 +170,7 @@ class ChallengeForm extends ViewModelWidget { ? viewModel.focusChallenge && challengeController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart similarity index 80% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart index 5af3ee4..a3fff2b 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/country_region_form_screen.dart @@ -5,10 +5,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_dropdown.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class CountryRegionForm extends ViewModelWidget { - const CountryRegionForm({super.key}); +class CountryRegionFormScreen extends ViewModelWidget { + const CountryRegionFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -25,7 +25,7 @@ class CountryRegionForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -42,7 +42,12 @@ class CountryRegionForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -62,13 +67,20 @@ class CountryRegionForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Where are you from?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -79,7 +91,7 @@ class CountryRegionForm extends ViewModelWidget { ); Widget _buildCountryDropDown(OnboardingViewModel viewModel) => - CustomDropDownPicker( + CustomDropdownPicker( onChanged: (value) {}, hint: 'Select country', icon: _buildSearchIcon(), @@ -88,7 +100,7 @@ class CountryRegionForm extends ViewModelWidget { ); Widget _buildRegionDropDown(OnboardingViewModel viewModel) => - CustomDropDownPicker( + CustomDropdownPicker( hint: 'Select region', onChanged: (value) {}, icon: _buildSearchIcon(), @@ -112,7 +124,7 @@ class CountryRegionForm extends ViewModelWidget { text: 'Continue', borderRadius: 12, onTap: () => viewModel.next(), - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart similarity index 81% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart index 963f474..8befcb4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/educational_background_form_screen.dart @@ -5,16 +5,16 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class EducationalBackgroundForm extends ViewModelWidget { - const EducationalBackgroundForm({super.key}); +class EducationalBackgroundFormScreen + extends ViewModelWidget { + const EducationalBackgroundFormScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -26,10 +26,15 @@ class EducationalBackgroundForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildColumnScroller(viewModel)); + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), @@ -60,13 +65,20 @@ class EducationalBackgroundForm extends ViewModelWidget { _buildEducationalLevels(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What’s your current educational level?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -110,12 +122,12 @@ class EducationalBackgroundForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedEducationalBackground != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedEducationalBackground != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart similarity index 78% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart index d153648..8a897d4 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/full_name_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class FullNameForm extends ViewModelWidget { +class FullNameFormScreen extends ViewModelWidget { final TextEditingController fullNameController; - const FullNameForm({super.key, required this.fullNameController}); + const FullNameFormScreen({super.key, required this.fullNameController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,7 +28,7 @@ class FullNameForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -45,7 +45,12 @@ class FullNameForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -66,13 +71,19 @@ class FullNameForm extends ViewModelWidget { _buildFullNameValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(showBackButton: false); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: false, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What should we call you? 😊', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -86,7 +97,10 @@ class FullNameForm extends ViewModelWidget { TextFormField( controller: fullNameController, onTap: viewModel.setFullNameFocus, - decoration: inputDecoration(focus: viewModel.focusFullName), + decoration: inputDecoration( + hint: 'Enter Your Name', + focus: viewModel.focusFullName, + filled: fullNameController.text.isNotEmpty), ); Widget _buildFullNameValidatorWrapper(OnboardingViewModel viewModel) => @@ -113,13 +127,13 @@ class FullNameForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.focusFullName && fullNameController.text.isNotEmpty ? () => viewModel.next() : null, backgroundColor: viewModel.focusFullName && fullNameController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart similarity index 83% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart index 1c3cc9a..3cb70fb 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_goal_form_screen.dart @@ -6,10 +6,10 @@ import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_large_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class LearningGoalForm extends ViewModelWidget { - const LearningGoalForm({super.key}); +class LearningGoalFormScreen extends ViewModelWidget { + const LearningGoalFormScreen({super.key}); IconData getIcon(int icon) { switch (icon) { @@ -38,11 +38,15 @@ class LearningGoalForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); + Widget _buildBodyScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyWrapper(viewModel), + ); Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: _buildBody(viewModel), @@ -70,13 +74,20 @@ class LearningGoalForm extends ViewModelWidget { _buildLearningGoals(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Hi Johnny, Choose your learning goal.', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -120,12 +131,12 @@ class LearningGoalForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedLearningGoal != null ? () => viewModel.next() : null, backgroundColor: viewModel.selectedLearningGoal != null ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart similarity index 79% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart index 09e9c62..064b05f 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/learning_reason_form_screen.dart @@ -6,12 +6,13 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class LearningReasonForm extends ViewModelWidget { +class LearningReasonFormScreen extends ViewModelWidget { final TextEditingController learningReasonController; - const LearningReasonForm({super.key, required this.learningReasonController}); + const LearningReasonFormScreen( + {super.key, required this.learningReasonController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,17 +29,19 @@ class LearningReasonForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -75,13 +78,18 @@ class LearningReasonForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + onPop: viewModel.pop, + showBackButton: true, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop(language: false)); Widget _buildTitle() => const Text( 'What’s your main goal for improving your English?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -121,7 +129,10 @@ class LearningReasonForm extends ViewModelWidget { maxLines: 3, controller: learningReasonController, onTap: viewModel.setLearningReasonFocus, - decoration: inputDecoration(focus: true, hint: 'Write your goal…'), + decoration: inputDecoration( + focus: true, + hint: 'Write your goal…', + filled: learningReasonController.text.isNotEmpty), ); Widget _buildReasonValidatorWrapper(OnboardingViewModel viewModel) => @@ -148,7 +159,7 @@ class LearningReasonForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedLearningReason != null ? viewModel.selectedLearningReason?.toLowerCase() == 'other' ? viewModel.focusLearningReason @@ -158,9 +169,10 @@ class LearningReasonForm extends ViewModelWidget { : null, backgroundColor: viewModel.selectedLearningReason != null ? viewModel.selectedLearningReason?.toLowerCase() == 'other' - ? viewModel.focusLearningReason && learningReasonController.text.isNotEmpty + ? viewModel.focusLearningReason && + learningReasonController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart similarity index 78% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart index f8f155d..19d08fd 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/occupation_form_screen.dart @@ -4,14 +4,14 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; import '../../onboarding_view.form.dart'; -class OccupationForm extends ViewModelWidget { +class OccupationFormScreen extends ViewModelWidget { final TextEditingController occupationController; - const OccupationForm({super.key, required this.occupationController}); + const OccupationFormScreen({super.key, required this.occupationController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -28,7 +28,7 @@ class OccupationForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -45,7 +45,12 @@ class OccupationForm extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -68,13 +73,20 @@ class OccupationForm extends ViewModelWidget { _buildOccupationValidatorWrapper(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'What’s your occupation?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -88,7 +100,10 @@ class OccupationForm extends ViewModelWidget { TextFormField( controller: occupationController, onTap: viewModel.setOccupationFocus, - decoration: inputDecoration(focus: viewModel.focusOccupation), + decoration: inputDecoration( + hint: 'Enter Your Occupation', + focus: viewModel.focusOccupation, + filled: occupationController.text.isNotEmpty), ); Widget _buildOccupationValidatorWrapper(OnboardingViewModel viewModel) => @@ -115,13 +130,13 @@ class OccupationForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.focusOccupation && occupationController.text.isNotEmpty ? () => viewModel.next() : null, backgroundColor: viewModel.focusOccupation && occupationController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2), + : kcPrimaryColor.withOpacity(0.1), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart similarity index 80% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart index f80cf4e..c38eaaa 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/forms/topic_form_screen.dart @@ -6,18 +6,17 @@ import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_view.form.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; -class TopicForm extends ViewModelWidget { +class TopicFormScreen extends ViewModelWidget { final TextEditingController topicController; - const TopicForm({super.key, required this.topicController}); + const TopicFormScreen({super.key, required this.topicController}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => _buildScaffoldWrapper(viewModel); - Widget _buildScaffoldWrapper(OnboardingViewModel viewModel) => Scaffold( backgroundColor: kcBackgroundColor, body: _buildScaffold(viewModel), @@ -29,17 +28,19 @@ class TopicForm extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => - Expanded(child: _buildBodyWrapper(viewModel)); + Expanded(child: _buildBodyScroller(viewModel)); - Widget _buildBodyWrapper(OnboardingViewModel viewModel) => + Widget _buildBodyScroller(OnboardingViewModel viewModel) => SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: _buildBody(viewModel), - ), + child: _buildBodyWrapper(viewModel), + ); + + Widget _buildBodyWrapper(OnboardingViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), ); Widget _buildBody(OnboardingViewModel viewModel) => Column( @@ -76,13 +77,20 @@ class TopicForm extends ViewModelWidget { verticalSpaceMedium, ]; - Widget _buildAppBar() => const OnboardingAppBar(); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: false, + )); Widget _buildTitle() => const Text( 'Which topics interest you most?', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -118,7 +126,10 @@ class TopicForm extends ViewModelWidget { maxLines: 3, controller: topicController, onTap: viewModel.setTopicsFocus, - decoration: inputDecoration(focus: true, hint: 'Write you interest…'), + decoration: inputDecoration( + focus: true, + hint: 'Write you interest…', + filled: topicController.text.isNotEmpty), ); Widget _buildTopicWrapper(OnboardingViewModel viewModel) => @@ -145,7 +156,7 @@ class TopicForm extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, onTap: viewModel.selectedTopic != null ? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.focusTopic @@ -157,7 +168,7 @@ class TopicForm extends ViewModelWidget { ? viewModel.selectedTopic?.toLowerCase() == 'other' ? viewModel.focusTopic && topicController.text.isNotEmpty ? kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2) + : kcPrimaryColor.withOpacity(0.1) : kcPrimaryColor - : kcPrimaryColor.withOpacity(0.2)); + : kcPrimaryColor.withOpacity(0.1)); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart index b4450e1..ebc05e6 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/language_selector.dart @@ -6,7 +6,7 @@ import '../../../common/app_colors.dart'; import '../../../common/ui_helpers.dart'; import '../../../widgets/custom_elevated_button.dart'; import '../../../widgets/custom_small_radio_button.dart'; -import '../../../widgets/onboarding_app_bar.dart'; +import '../../../widgets/large_app_bar.dart'; class LanguageSelector extends ViewModelWidget { const LanguageSelector({Key? key}) : super(key: key); @@ -26,7 +26,7 @@ class LanguageSelector extends ViewModelWidget { ); List _buildScaffoldChildren(OnboardingViewModel viewModel) => - [_buildAppBar(), _buildExpandedBody(viewModel)]; + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; Widget _buildExpandedBody(OnboardingViewModel viewModel) => Expanded(child: _buildBodyWrapper(viewModel)); @@ -43,7 +43,12 @@ class LanguageSelector extends ViewModelWidget { ); List _buildBodyChildren(OnboardingViewModel viewModel) => - [_buildUpperColumn(viewModel), _buildContinueButtonWrapper(viewModel)]; + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(OnboardingViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); Widget _buildUpperColumn(OnboardingViewModel viewModel) => Column( mainAxisSize: MainAxisSize.min, @@ -60,13 +65,20 @@ class LanguageSelector extends ViewModelWidget { _buildLanguages(viewModel) ]; - Widget _buildAppBar() => const OnboardingAppBar(language: true); + Widget _buildAppBar(OnboardingViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.pop, + showLanguageSelection: true, + onLanguage: () => viewModel.next(page: 23), + onTap: () => viewModel.pop( + language: true, + )); Widget _buildTitle() => const Text( 'Choose your language', style: TextStyle( fontSize: 25, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w600, ), ); @@ -109,7 +121,7 @@ class LanguageSelector extends ViewModelWidget { height: 55, text: 'Continue', borderRadius: 12, - foregroundColor: kcWhiteColor, + foregroundColor: kcWhite, backgroundColor: kcPrimaryColor, onTap: () => viewModel.pop(language: true), ); diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart index f99dbf0..fe16c18 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/first_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class FirstWelcome extends ViewModelWidget { - const FirstWelcome({super.key}); +class FirstWelcomeScreen extends ViewModelWidget { + const FirstWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -55,14 +53,17 @@ class FirstWelcome extends ViewModelWidget { _buildTitle(), ]; - Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); + Widget _buildIcon() => SvgPicture.asset( + 'assets/icons/logo.svg', + height: 50, + ); Widget _buildTitle() => const Text( 'Small daily practice. Big lifelong results.', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -80,11 +81,11 @@ class FirstWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart similarity index 88% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart index 2b49b96..487866d 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/second_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class SecondWelcome extends ViewModelWidget { - const SecondWelcome({super.key}); +class SecondWelcomeScreen extends ViewModelWidget { + const SecondWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -57,6 +55,7 @@ class SecondWelcome extends ViewModelWidget { Widget _buildIcon() => SvgPicture.asset( 'assets/icons/logo.svg', + height: 50, ); Widget _buildTitle() => const Text( @@ -64,7 +63,7 @@ class SecondWelcome extends ViewModelWidget { textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -82,11 +81,11 @@ class SecondWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart similarity index 86% rename from StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart rename to StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart index 7702dda..cb590ac 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/onboarding/screens/welcome/third_welcome_screen.dart @@ -5,11 +5,9 @@ import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; import 'package:yimaru_app/ui/widgets/custom_elevated_button.dart'; import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/custom_small_radio_button.dart'; -import 'package:yimaru_app/ui/widgets/onboarding_app_bar.dart'; -class ThirdWelcome extends ViewModelWidget { - const ThirdWelcome({super.key}); +class ThirdWelcomeScreen extends ViewModelWidget { + const ThirdWelcomeScreen({super.key}); @override Widget build(BuildContext context, OnboardingViewModel viewModel) => @@ -54,15 +52,17 @@ class ThirdWelcome extends ViewModelWidget { verticalSpaceMedium, _buildTitle(), ]; - - Widget _buildIcon() => SvgPicture.asset('assets/icons/logo.svg'); + Widget _buildIcon() => SvgPicture.asset( + 'assets/icons/logo.svg', + height: 50, + ); Widget _buildTitle() => const Text( 'Every conversation brings you closer to the life you want.', textAlign: TextAlign.center, style: TextStyle( fontSize: 30, - color: kcWhiteColor, + color: kcWhite, fontWeight: FontWeight.w600, ), ); @@ -80,11 +80,11 @@ class ThirdWelcome extends ViewModelWidget { Widget _buildContinueButton(OnboardingViewModel viewModel) => CustomElevatedButton( height: 55, - icon: true, borderRadius: 12, text: 'Start Learning', + backgroundColor: kcWhite, + trailingIcon: Icons.arrow_forward, onTap: () => viewModel.next(), - backgroundColor: kcWhiteColor, foregroundColor: kcPrimaryColor, ); } diff --git a/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart new file mode 100644 index 0000000..4cd3fc3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_view.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/course_progress_section.dart'; +import 'package:yimaru_app/ui/widgets/learning_progress_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'ongoing_progress_viewmodel.dart'; + +class OngoingProgressView extends StackedView { + const OngoingProgressView({Key? key}) : super(key: key); + + @override + OngoingProgressViewModel viewModelBuilder( + BuildContext context, + ) => + OngoingProgressViewModel(); + + @override + Widget builder( + BuildContext context, + OngoingProgressViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(OngoingProgressViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(OngoingProgressViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(OngoingProgressViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(OngoingProgressViewModel viewModel) => + _buildNestedScrollView(viewModel); + + Widget _buildNestedScrollView(OngoingProgressViewModel viewModel) => + NestedScrollView( + scrollDirection: Axis.vertical, + headerSliverBuilder: + (BuildContext context, bool innerBoxIsScrolled) => + [_buildSliverAppbarWrapper(viewModel)], + body: _buildContentScrollViewWrapper(viewModel)); + + Widget _buildSliverAppbarWrapper(OngoingProgressViewModel viewModel) => + SliverAppBar( + pinned: true, + automaticallyImplyLeading: false, + backgroundColor: kcBackgroundColor, + surfaceTintColor: kcBackgroundColor, + title: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(OngoingProgressViewModel viewModel) => SmallAppBar( + title: 'My Progress', + onTap: viewModel.pop, + ); + + Widget _buildContentScrollViewWrapper(OngoingProgressViewModel viewModel) => + SingleChildScrollView( + child: _buildContentWrapper(viewModel), + ); + + Widget _buildContentWrapper(OngoingProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(OngoingProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(OngoingProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildText(), + verticalSpaceMedium, + _buildLearningProgressCard(), + verticalSpaceMedium, + _buildCourseProgressSection() + ]; + + Widget _buildText() => const Text( + 'Track your learning journey and see your growth over time.', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildLearningProgressCard() => const LearningProgressCard(); + + Widget _buildCourseProgressSection() => const CourseProgressSection(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart new file mode 100644 index 0000000..0c3c308 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart @@ -0,0 +1,21 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class OngoingProgressViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _courses = [ + { + 'title': 'IELTS Preparation', + }, + { + 'title': 'Duolingo English Test', + }, + ]; + + List> get courses => _courses; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart new file mode 100644 index 0000000..f4a04f5 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_view.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/privacy_policy_tile.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'privacy_policy_viewmodel.dart'; + +class PrivacyPolicyView extends StackedView { + const PrivacyPolicyView({Key? key}) : super(key: key); + + @override + PrivacyPolicyViewModel viewModelBuilder(BuildContext context) => + PrivacyPolicyViewModel(); + + @override + Widget builder( + BuildContext context, + PrivacyPolicyViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(PrivacyPolicyViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(PrivacyPolicyViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(PrivacyPolicyViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(PrivacyPolicyViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(PrivacyPolicyViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(PrivacyPolicyViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(PrivacyPolicyViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(PrivacyPolicyViewModel viewModel) => SmallAppBar( + title: 'Privacy Policy', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(PrivacyPolicyViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(PrivacyPolicyViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(PrivacyPolicyViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(PrivacyPolicyViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(PrivacyPolicyViewModel viewModel) => + [verticalSpaceLarge, _buildListView(viewModel)]; + + Widget _buildListView(PrivacyPolicyViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.privacyPolicies.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => + _buildTile(viewModel.privacyPolicies[index]['title']), + ); + + Widget _buildTile(String title) => PrivacyPolicyTile(title: title); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart new file mode 100644 index 0000000..11c56a6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/privacy_policy/privacy_policy_viewmodel.dart @@ -0,0 +1,23 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class PrivacyPolicyViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Privacy policy + final List> _privacyPolicies = [ + {'title': 'Introduction'}, + {'title': 'Information We Collect'}, + {'title': 'How We Use Your Information'}, + {'title': 'Data Sharing and Disclosure'}, + {'title': 'Your Rights and Choices'}, + {'title': 'Data Security'} + ]; + + List> get privacyPolicies => _privacyPolicies; + + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart new file mode 100644 index 0000000..fc79f97 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_view.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/profile_card.dart'; +import 'package:yimaru_app/ui/widgets/profile_image.dart'; +import 'package:yimaru_app/ui/widgets/view_profile_button.dart'; + +import '../../widgets/custom_elevated_button.dart'; +import 'profile_viewmodel.dart'; + +class ProfileView extends StackedView { + const ProfileView({Key? key}) : super(key: key); + + @override + ProfileViewModel viewModelBuilder( + BuildContext context, + ) => + ProfileViewModel(); + + @override + Widget builder( + BuildContext context, + ProfileViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProfileViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProfileViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProfileViewModel viewModel) => SingleChildScrollView( + child: _buildBody(viewModel), + ); + + Widget _buildBody(ProfileViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(ProfileViewModel viewModel) => Column( + children: [ + verticalSpaceMedium, + _buildNotificationIconWrapper(), + _buildProfileSection(), + verticalSpaceSmall, + _buildViewProfileButton(viewModel), + verticalSpaceLarge, + _buildSettingsSection(viewModel), + verticalSpaceLarge, + _buildLogOutButton(viewModel), + verticalSpaceLarge, + ], + ); + + Widget _buildNotificationIconWrapper() => + Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon()); + + Widget _buildNotificationIcon() => const Icon( + Icons.notifications_none, + color: kcDarkGrey, + ); + + Widget _buildProfileSection() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildProfileSectionChildren(), + ); + + List _buildProfileSectionChildren() => [ + _buildProfileImage(), + verticalSpaceSmall, + _buildProfileName(), + ]; + + Widget _buildProfileImage() => const ProfileImage(); + + Widget _buildProfileName() => const Text( + 'Hi, Bisrat 👋', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildViewProfileButton(ProfileViewModel viewModel) => + ViewProfileButton( + onTap: () async => await viewModel.navigateToProfileDetail(), + ); + + Widget _buildSettingsSection(ProfileViewModel viewModel) => GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, mainAxisSpacing: 15, crossAxisSpacing: 15), + children: _buildSettingsChildren(viewModel)); + + List _buildSettingsChildren(ProfileViewModel viewModel) => [ + _buildDownloadsCard(viewModel), + _buildProgressCard(viewModel), + _buildAccountCard(viewModel), + _buildSupportCard(viewModel) + ]; + + Widget _buildDownloadsCard(ProfileViewModel viewModel) => ProfileCard( + icon: Icons.download, + title: 'My Downloads', + subTitle: 'Access offline lessons and saved videos', + onTap: () async => await viewModel.navigateToDownloads(), + ); + + Widget _buildProgressCard(ProfileViewModel viewModel) => ProfileCard( + title: 'My Progress', + icon: Icons.stacked_bar_chart, + subTitle: 'Track your achievements and learning streak', + onTap: () async => await viewModel.navigateToProgress(), + ); + + Widget _buildAccountCard(ProfileViewModel viewModel) => ProfileCard( + title: 'Account & Privacy', + icon: Icons.privacy_tip_outlined, + subTitle: 'Manage setting and app preference', + onTap: () async => await viewModel.navigateToAccountPrivacy(), + ); + + Widget _buildSupportCard(ProfileViewModel viewModel) => ProfileCard( + title: 'Support', + icon: Icons.headphones, + subTitle: 'Get help through phone or Telegram', + onTap: () async => await viewModel.navigateToSupport(), + ); + + Widget _buildLogOutButton(ProfileViewModel viewModel) => CustomElevatedButton( + height: 55, + text: 'Log Out', + borderRadius: 12, + foregroundColor: kcRed, + onTap: () async => await viewModel.logOut(), + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart new file mode 100644 index 0000000..ec35e68 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile/profile_viewmodel.dart @@ -0,0 +1,32 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; +import '../../../services/authentication_service.dart'; + +class ProfileViewModel extends BaseViewModel { + final _navigationService = locator(); + + final _authenticationService = locator(); + + Future logOut() async { + await _authenticationService.logOut(); + await _navigationService.replaceWithLoginView(); + } + + Future navigateToProfileDetail() async => + await _navigationService.navigateToProfileDetailView(); + + Future navigateToDownloads() async => + await _navigationService.navigateToDownloadsView(); + + Future navigateToProgress() async => + await _navigationService.navigateToProgressView(); + + Future navigateToAccountPrivacy() async => + await _navigationService.navigateToAccountPrivacyView(); + + Future navigateToSupport() async => + await _navigationService.navigateToSupportView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart new file mode 100644 index 0000000..d7b64a7 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.dart @@ -0,0 +1,569 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/widgets/birthday_selector.dart'; +import 'package:yimaru_app/ui/widgets/custom_form_label.dart'; +import 'package:yimaru_app/ui/widgets/small_app_bar.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../common/validators/form_validator.dart'; +import '../../widgets/custom_dropdown.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/profile_image.dart'; +import 'profile_detail_viewmodel.dart'; + +import 'profile_detail_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'email', validator: FormValidator.validateForm), + FormTextField( + name: 'phoneNumber', validator: FormValidator.validatePhoneNumber), + FormTextField(name: 'lastName', validator: FormValidator.validateForm), + FormTextField(name: 'firstName', validator: FormValidator.validateForm), +]) +class ProfileDetailView extends StackedView + with $ProfileDetailView { + const ProfileDetailView({Key? key}) : super(key: key); + + void _onModelReady() { + firstNameController.text = 'Abel'; + lastNameController.text = 'Abebe'; + phoneNumberController.text = '251900000000'; + emailController.text = 'email@test.com'; + } + + @override + void onViewModelReady(ProfileDetailViewModel viewModel) { + _onModelReady(); + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + ProfileDetailViewModel viewModelBuilder(BuildContext context) => + ProfileDetailViewModel(); + + @override + Widget builder( + BuildContext context, + ProfileDetailViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProfileDetailViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProfileDetailViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProfileDetailViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(ProfileDetailViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppbar(viewModel), + verticalSpaceSmall, + _buildColumnWrapper(viewModel) + ]; + + Widget _buildAppbar(ProfileDetailViewModel viewModel) => SmallAppBar( + title: 'Edit Profile', + onTap: viewModel.pop, + ); + + Widget _buildColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildBodyColumn(viewModel)); + + Widget _buildBodyColumn(ProfileDetailViewModel viewModel) => + SingleChildScrollView( + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(ProfileDetailViewModel viewModel) => [ + verticalSpaceMedium, + _buildProfileImage(), + verticalSpaceMedium, + _buildNameFormSection(viewModel), + verticalSpaceMedium, + _buildGenderFormFieldWrapper(viewModel), + verticalSpaceSmall, + _buildBirthdayColumn(viewModel), + verticalSpaceSmall, + _buildPhoneNumberFormFieldSection(viewModel), + verticalSpaceTiny, + _buildEmailFormFieldSection(viewModel), + verticalSpaceMedium, + _buildCountryRegionSection(viewModel), + verticalSpaceMedium, + _buildOccupationDropdownWrapper(viewModel), + verticalSpaceLarge, + _buildLowerColumn(viewModel) + ]; + + Widget _buildProfileImage() => + const Align(alignment: Alignment.center, child: ProfileImage()); + + Widget _buildNameFormSection(ProfileDetailViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildNameFormChildren(viewModel), + ); + + List _buildNameFormChildren(ProfileDetailViewModel viewModel) => [ + _buildFirstNameFormFieldWrapper(viewModel), + const SizedBox(width: 20), + _buildLastNameFormFieldWrapper(viewModel) + ]; + + Widget _buildFirstNameFormFieldWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildFirstNameFormFieldColumn(viewModel)); + + Widget _buildFirstNameFormFieldColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildFirstNameFormFieldChildren(viewModel), + ); + + List _buildFirstNameFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildFirstNameLabel(), + verticalSpaceSmall, + _buildFirstNameFormField(viewModel), + if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName) + verticalSpaceTiny, + if (viewModel.hasFirstNameValidationMessage && viewModel.focusFirstName) + _buildFirstNameValidatorWrapper(viewModel) + ]; + + Widget _buildFirstNameLabel() => CustomFormLabel( + label: 'First Name', + style: style16DG600, + ); + + Widget _buildFirstNameFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: firstNameController, + onTap: viewModel.setFirstNameFocus, + decoration: inputDecoration( + focus: viewModel.focusFirstName, + filled: firstNameController.text.isNotEmpty), + ); + + Widget _buildFirstNameValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasFirstNameValidationMessage + ? _buildFirstNameValidator(viewModel) + : Container(); + + Widget _buildFirstNameValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.firstNameValidationMessage!, + style: validationStyle, + ); + + Widget _buildLastNameFormFieldWrapper(ProfileDetailViewModel viewModel) => + Expanded(child: _buildLastNameFormFieldColumn(viewModel)); + + Widget _buildLastNameFormFieldColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildLastNameFormFieldChildren(viewModel), + ); + + List _buildLastNameFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildLastNameLabel(), + verticalSpaceSmall, + _buildLastNameFormField(viewModel), + if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName) + verticalSpaceTiny, + if (viewModel.hasLastNameValidationMessage && viewModel.focusLastName) + _buildLastNameValidatorWrapper(viewModel) + ]; + + Widget _buildLastNameLabel() => CustomFormLabel( + label: 'Last Name', + style: style16DG600, + ); + + Widget _buildLastNameFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: lastNameController, + onTap: viewModel.setLastNameFocus, + decoration: inputDecoration( + focus: viewModel.focusLastName, + filled: lastNameController.text.isNotEmpty), + ); + + Widget _buildLastNameValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasLastNameValidationMessage + ? _buildLastNameValidator(viewModel) + : Container(); + + Widget _buildLastNameValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.lastNameValidationMessage!, + style: validationStyle, + ); + + Widget _buildGenderFormFieldWrapper(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildGenderFormFieldChildren(viewModel), + ); + + List _buildGenderFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildGenderLabel(), + verticalSpaceTiny, + _buildRadioButtonWrapper(viewModel), + ]; + + Widget _buildGenderLabel() => CustomFormLabel( + label: 'Gender', + style: style16DG600, + ); + + Widget _buildRadioButtonWrapper(ProfileDetailViewModel viewModel) => Row( + mainAxisSize: MainAxisSize.min, + children: _buildRadioButtonChildren(viewModel), + ); + + List _buildRadioButtonChildren(ProfileDetailViewModel viewModel) => + [_buildMaleRadioButton(viewModel), _buildFemaleRadioButton(viewModel)]; + + Widget _buildMaleRadioButton(ProfileDetailViewModel viewModel) => + RadioGroup( + groupValue: viewModel.selectedGender, + onChanged: (value) => viewModel.setGender(value ?? ''), + child: _buildMaleRadioTileWrapper(viewModel)); + + Widget _buildMaleRadioTileWrapper(ProfileDetailViewModel viewModel) => + Container( + width: 125, + alignment: Alignment.centerLeft, + child: _buildMaleRadioTile(viewModel)); + + Widget _buildMaleRadioTile(ProfileDetailViewModel viewModel) => + RadioListTile( + value: 'Male', + title: _buildMaleTitle(), + activeColor: kcPrimaryColor, + contentPadding: EdgeInsets.zero, + ); + + Widget _buildMaleTitle() => const Text( + 'Male', + style: TextStyle( + fontSize: 14, + color: kcDarkGrey, + fontWeight: FontWeight.w500, + ), + ); + + Widget _buildFemaleRadioButton(ProfileDetailViewModel viewModel) => + RadioGroup( + groupValue: viewModel.selectedGender, + onChanged: (value) => viewModel.setGender(value ?? ''), + child: _buildFemaleRadioTileWrapper(viewModel)); + + Widget _buildFemaleRadioTileWrapper(ProfileDetailViewModel viewModel) => + Container( + width: 125, + alignment: Alignment.centerLeft, + child: _buildFemaleRadioTile(viewModel), + ); + + Widget _buildFemaleRadioTile(ProfileDetailViewModel viewModel) => + RadioListTile( + value: 'Female', + title: _buildFemaleTitle(), + activeColor: kcPrimaryColor, + contentPadding: EdgeInsets.zero, + ); + + Widget _buildFemaleTitle() => const Text( + 'Female', + style: TextStyle( + fontSize: 14, + color: kcDarkGrey, + fontWeight: FontWeight.w500, + ), + ); + + Widget _buildBirthdayColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBirthdayChildren(viewModel), + ); + + List _buildBirthdayChildren(ProfileDetailViewModel viewModel) => [ + _buildBirthdayLabel(), + verticalSpaceSmall, + _buildBirthdayFormField(), + ]; + + Widget _buildBirthdayLabel() => CustomFormLabel( + label: 'Birthday', + style: style16DG600, + ); + + Widget _buildBirthdayFormField() => const BirthdaySelector(); + + Widget _buildPhoneNumberFormFieldSection(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberFormFieldChildren(viewModel), + ); + + List _buildPhoneNumberFormFieldChildren( + ProfileDetailViewModel viewModel) => + [ + _buildPhoneNumberLabel(), + verticalSpaceSmall, + _buildPhoneNumberFormField(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel) + ]; + + Widget _buildPhoneNumberLabel() => CustomFormLabel( + label: 'Phone Number', + style: style16DG600, + ); + + Widget _buildPhoneNumberFormField(ProfileDetailViewModel viewModel) => + TextFormField( + maxLength: 12, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + hint: '251', + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: validationStyle, + ); + + Widget _buildEmailFormFieldSection(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildEmailFormFieldChildren(viewModel), + ); + + List _buildEmailFormFieldChildren(ProfileDetailViewModel viewModel) => + [ + _buildEmailLabel(), + verticalSpaceSmall, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel) + ]; + + Widget _buildEmailLabel() => CustomFormLabel( + label: 'Email', + style: style16DG600, + ); + + Widget _buildEmailFormField(ProfileDetailViewModel viewModel) => + TextFormField( + controller: emailController, + onTap: viewModel.setPhoneNumberFocus, + keyboardType: TextInputType.emailAddress, + decoration: inputDecoration( + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(ProfileDetailViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(ProfileDetailViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: validationStyle, + ); + + Widget _buildCountryRegionSection(ProfileDetailViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildCountryRegionChildren(viewModel), + ); + + List _buildCountryRegionChildren(ProfileDetailViewModel viewModel) => + [ + _buildCountryDropdownColumnWrapper(viewModel), + const SizedBox(width: 20), + _buildRegionDropdownColumnWrapper(viewModel) + ]; + + Widget _buildCountryDropdownColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded( + child: _buildCountryDropdownColumn(viewModel), + ); + + Widget _buildCountryDropdownColumn(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildCountryDropdownChildren(viewModel), + ); + + List _buildCountryDropdownChildren( + ProfileDetailViewModel viewModel) => + [ + _buildCountryDropdownLabel(), + verticalSpaceSmall, + _buildCountryDropdown(viewModel) + ]; + + Widget _buildCountryDropdownLabel() => CustomFormLabel( + label: 'Country', + style: style16DG600, + ); + + Widget _buildCountryDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + onChanged: (value) {}, + hint: 'Select country', + selectedItem: 'Ethiopia', + items: (value, props) => viewModel.getCountries(), + ); + + Widget _buildRegionDropdownColumnWrapper(ProfileDetailViewModel viewModel) => + Expanded( + child: _buildRegionDropdownColumn(viewModel), + ); + + Widget _buildRegionDropdownColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildRegionDropdownChildren(viewModel), + ); + + List _buildRegionDropdownChildren(ProfileDetailViewModel viewModel) => + [ + _buildRegionDropdownLabel(), + verticalSpaceSmall, + _buildRegionDropdown(viewModel) + ]; + + Widget _buildRegionDropdownLabel() => CustomFormLabel( + label: 'Region', + style: style16DG600, + ); + + Widget _buildRegionDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + hint: 'Select region', + onChanged: (value) {}, + selectedItem: 'Addis Ababa', + items: (value, props) => viewModel.getRegions('Addis Ababa'), + ); + + Widget _buildOccupationDropdownWrapper(ProfileDetailViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: _buildOccupationDropdownChildren(viewModel), + ); + + List _buildOccupationDropdownChildren( + ProfileDetailViewModel viewModel) => + [ + _buildOccupationDropdownLabel(), + verticalSpaceSmall, + _buildOccupationDropdown(viewModel) + ]; + + Widget _buildOccupationDropdownLabel() => CustomFormLabel( + label: 'Occupation', + style: style16DG600, + ); + + Widget _buildOccupationDropdown(ProfileDetailViewModel viewModel) => + CustomDropdownPicker( + hint: 'Select occupation', + onChanged: (value) {}, + selectedItem: 'Student', + items: (value, props) => viewModel.getOccupations('Student'), + ); + + Widget _buildLowerColumn(ProfileDetailViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(ProfileDetailViewModel viewModel) => [ + _buildSaveButton(viewModel), + verticalSpaceSmall, + _buildCancelButtonWrapper(viewModel) + ]; + + Widget _buildSaveButton(ProfileDetailViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Save Changes', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); + + Widget _buildCancelButtonWrapper(ProfileDetailViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildCancelButton(viewModel), + ); + + Widget _buildCancelButton(ProfileDetailViewModel viewModel) => + const CustomElevatedButton( + height: 55, + text: 'Cancel', + borderRadius: 12, + borderColor: kcPrimaryColor, + backgroundColor: kcWhite, + foregroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart new file mode 100644 index 0000000..d5e8df2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_view.form.dart @@ -0,0 +1,278 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String EmailValueKey = 'email'; +const String PhoneNumberValueKey = 'phoneNumber'; +const String LastNameValueKey = 'lastName'; +const String FirstNameValueKey = 'firstName'; + +final Map + _ProfileDetailViewTextEditingControllers = {}; + +final Map _ProfileDetailViewFocusNodes = {}; + +final Map + _ProfileDetailViewTextValidations = { + EmailValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validatePhoneNumber, + LastNameValueKey: FormValidator.validateForm, + FirstNameValueKey: FormValidator.validateForm, +}; + +mixin $ProfileDetailView { + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + TextEditingController get lastNameController => + _getFormTextEditingController(LastNameValueKey); + TextEditingController get firstNameController => + _getFormTextEditingController(FirstNameValueKey); + + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + FocusNode get lastNameFocusNode => _getFormFocusNode(LastNameValueKey); + FocusNode get firstNameFocusNode => _getFormFocusNode(FirstNameValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_ProfileDetailViewTextEditingControllers.containsKey(key)) { + return _ProfileDetailViewTextEditingControllers[key]!; + } + + _ProfileDetailViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _ProfileDetailViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_ProfileDetailViewFocusNodes.containsKey(key)) { + return _ProfileDetailViewFocusNodes[key]!; + } + _ProfileDetailViewFocusNodes[key] = FocusNode(); + return _ProfileDetailViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + emailController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + lastNameController.addListener(() => _updateFormData(model)); + firstNameController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + emailController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + lastNameController.addListener(() => _updateFormData(model)); + firstNameController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + EmailValueKey: emailController.text, + PhoneNumberValueKey: phoneNumberController.text, + LastNameValueKey: lastNameController.text, + FirstNameValueKey: firstNameController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _ProfileDetailViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _ProfileDetailViewFocusNodes.values) { + focusNode.dispose(); + } + + _ProfileDetailViewTextEditingControllers.clear(); + _ProfileDetailViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + String? get lastNameValue => this.formValueMap[LastNameValueKey] as String?; + String? get firstNameValue => this.formValueMap[FirstNameValueKey] as String?; + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey(EmailValueKey)) { + _ProfileDetailViewTextEditingControllers[EmailValueKey]?.text = + value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + PhoneNumberValueKey)) { + _ProfileDetailViewTextEditingControllers[PhoneNumberValueKey]?.text = + value ?? ''; + } + } + + set lastNameValue(String? value) { + this.setData( + this.formValueMap..addAll({LastNameValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + LastNameValueKey)) { + _ProfileDetailViewTextEditingControllers[LastNameValueKey]?.text = + value ?? ''; + } + } + + set firstNameValue(String? value) { + this.setData( + this.formValueMap..addAll({FirstNameValueKey: value}), + ); + + if (_ProfileDetailViewTextEditingControllers.containsKey( + FirstNameValueKey)) { + _ProfileDetailViewTextEditingControllers[FirstNameValueKey]?.text = + value ?? ''; + } + } + + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + bool get hasLastName => + this.formValueMap.containsKey(LastNameValueKey) && + (lastNameValue?.isNotEmpty ?? false); + bool get hasFirstName => + this.formValueMap.containsKey(FirstNameValueKey) && + (firstNameValue?.isNotEmpty ?? false); + + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + bool get hasLastNameValidationMessage => + this.fieldsValidationMessages[LastNameValueKey]?.isNotEmpty ?? false; + bool get hasFirstNameValidationMessage => + this.fieldsValidationMessages[FirstNameValueKey]?.isNotEmpty ?? false; + + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; + String? get lastNameValidationMessage => + this.fieldsValidationMessages[LastNameValueKey]; + String? get firstNameValidationMessage => + this.fieldsValidationMessages[FirstNameValueKey]; +} + +extension Methods on FormStateHelper { + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + setLastNameValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[LastNameValueKey] = validationMessage; + setFirstNameValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[FirstNameValueKey] = validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + emailValue = ''; + phoneNumberValue = ''; + lastNameValue = ''; + firstNameValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + LastNameValueKey: getValidationMessage(LastNameValueKey), + FirstNameValueKey: getValidationMessage(FirstNameValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _ProfileDetailViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _ProfileDetailViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + EmailValueKey: getValidationMessage(EmailValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + LastNameValueKey: getValidationMessage(LastNameValueKey), + FirstNameValueKey: getValidationMessage(FirstNameValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart new file mode 100644 index 0000000..16769e6 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/profile_detail/profile_detail_viewmodel.dart @@ -0,0 +1,86 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class ProfileDetailViewModel extends FormViewModel { + final _navigationService = locator(); + // First name + bool _focusFirstName = false; + + bool get focusFirstName => _focusFirstName; + + // Last name + bool _focusLastName = false; + + bool get focusLastName => _focusLastName; + + // Gender + String? _selectedGender; + + String? get selectedGender => _selectedGender; + + // Birthday + String? _selectedBirthday; + + String? get selectedBirthday => _selectedBirthday; + + // First name + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // First name + void setFirstNameFocus() { + _focusFirstName = true; + rebuildUi(); + } + + // Last name + void setLastNameFocus() { + _focusLastName = true; + rebuildUi(); + } + + // Gender + void setGender(String value) { + _selectedGender = value; + rebuildUi(); + } + + // Birthday + void setBirthday(String value) { + _selectedBirthday = value; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Country + Future> getCountries() async => ['Ethiopia', 'Djibouti']; + + // Region + Future> getRegions(String country) async => + ['Addis Ababa', 'Oromia']; + + // Occupation + Future> getOccupations(String country) async => + ['Student', 'Worker']; + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart new file mode 100644 index 0000000..4b90527 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_view.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/course_level_card.dart'; +import 'package:yimaru_app/ui/widgets/skill_progress.dart'; +import 'package:yimaru_app/ui/widgets/suggestion_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'progress_viewmodel.dart'; + +class ProgressView extends StackedView { + const ProgressView({Key? key}) : super(key: key); + + @override + ProgressViewModel viewModelBuilder(BuildContext context) => + ProgressViewModel(); + + @override + Widget builder( + BuildContext context, + ProgressViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(ProgressViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(ProgressViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(ProgressViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(ProgressViewModel viewModel) => _buildColumn(viewModel); + + Widget _buildColumn(ProgressViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(ProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(ProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(ProgressViewModel viewModel) => SmallAppBar( + title: 'My Progress', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(ProgressViewModel viewModel) => + Expanded(child: _buildContentScrollView(viewModel)); + + Widget _buildContentScrollView(ProgressViewModel viewModel) => + SingleChildScrollView( + child: _buildContentColumn(viewModel), + ); + + Widget _buildContentColumn(ProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentChildren(viewModel), + ); + + List _buildContentChildren(ProgressViewModel viewModel) => [ + verticalSpaceMedium, + _buildCourseProgressSection(viewModel), + verticalSpaceMedium, + _buildSkillTitleWrapper(), + verticalSpaceMedium, + _buildSkillsWrapper(viewModel), + verticalSpaceLarge, + _buildSuggestionCard(), + verticalSpaceMassive + ]; + + Widget _buildCourseProgressSection(ProgressViewModel viewModel) => SizedBox( + height: 250, + width: double.maxFinite, + child: _buildListView(viewModel), + ); + + Widget _buildListView(ProgressViewModel viewModel) => ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: viewModel.progresses.length, + controller: PageController(viewportFraction: 0.9), + itemBuilder: (context, index) => _buildCourseLeveCard( + viewModel: viewModel, + icon: viewModel.progresses[index]['icon'], + title: viewModel.progresses[index]['title'], + color: viewModel.progresses[index]['color'], + status: viewModel.progresses[index]['status'], + subTitle: viewModel.progresses[index]['subTitle'], + isCompleted: viewModel.progresses[index]['isCompleted'], + ), + ); + + Widget _buildCourseLeveCard( + {required Color color, + required String title, + required String icon, + required String status, + required String subTitle, + required bool isCompleted, + required ProgressViewModel viewModel}) => + CourseLevelCard( + icon: icon, + title: title, + color: color, + status: status, + subTitle: subTitle, + isCompleted: isCompleted, + onTap: viewModel.navigateToOngoingProgress, + ); + + Widget _buildSkillTitleWrapper() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildSkillTitle(), + ); + + Widget _buildSkillTitle() => const Text( + 'Skill Proficiency', + style: TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSkillsWrapper(ProgressViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildSkills(viewModel), + ); + + Widget _buildSkills(ProgressViewModel viewModel) => ListView.builder( + shrinkWrap: true, + itemCount: viewModel.skillsLevel.length, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) => _buildSkill( + skill: viewModel.skillsLevel[index]['skill'], + progress: viewModel.skillsLevel[index]['progress'], + ), + ); + + Widget _buildSkill({ + required String skill, + required double progress, + }) => + SkillProgress( + skill: skill, + progress: progress, + ); + + Widget _buildSuggestionCard() => const SuggestionCard(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart new file mode 100644 index 0000000..042b9ea --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/progress/progress_viewmodel.dart @@ -0,0 +1,65 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +import '../../../app/app.locator.dart'; + +class ProgressViewModel extends BaseViewModel { + final _navigationService = locator(); + + final List> _progresses = [ + { + 'color': kcGreen, + 'title': 'Beginner', + 'isCompleted': true, + 'status': 'Completed', + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'You’ve mastered everyday English basics!', + }, + { + 'title': 'Elementary', + 'isCompleted': false, + 'status': 'In Progress', + 'color': kcPrimaryColor, + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'Continue improving your conversations and fluency.', + }, + { + 'title': 'Beginner', + 'isCompleted': true, + 'status': 'In Progress', + 'color': kcPrimaryColor, + 'icon': 'assets/icons/b1.svg', + 'subTitle': 'You’ve mastered everyday English basics!', + }, + ]; + + List> get progresses => _progresses; + + final List> _skillsLevel = [ + { + 'progress': 0.8, + 'skill': 'Speaking', + }, + { + 'progress': 0.95, + 'skill': 'Listening', + }, + { + 'progress': 0.75, + 'skill': 'Writing', + }, + { + 'progress': 0.8, + 'skill': 'Reading', + }, + ]; + + List> get skillsLevel => _skillsLevel; + + Future navigateToOngoingProgress() async => + await _navigationService.navigateToOngoingProgressView(); + + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart new file mode 100644 index 0000000..e84a96b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked/stacked_annotations.dart'; +import 'package:yimaru_app/ui/views/register/screens/create_password_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/register_with_email_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/register_with_phone_number_screen.dart'; +import 'package:yimaru_app/ui/views/register/screens/registration_otp_screen.dart'; +import 'package:yimaru_app/ui/widgets/large_app_bar.dart'; +import 'package:yimaru_app/ui/widgets/page_loading_indicator.dart'; + +import '../../common/app_colors.dart'; +import '../../common/validators/form_validator.dart'; +import 'register_viewmodel.dart'; + +import 'register_view.form.dart'; + +@FormView(fields: [ + FormTextField(name: 'otp', validator: FormValidator.validateForm), + FormTextField(name: 'email', validator: FormValidator.validateEmail), + FormTextField(name: 'password', validator: FormValidator.validateForm), + FormTextField(name: 'phoneNumber', validator: FormValidator.validateForm), + FormTextField(name: 'confirmPassword', validator: FormValidator.validateForm), +]) +class RegisterView extends StackedView with $RegisterView { + const RegisterView({Key? key}) : super(key: key); + + @override + void onViewModelReady(RegisterViewModel viewModel) { + syncFormWithViewModel(viewModel); + super.onViewModelReady(viewModel); + } + + @override + RegisterViewModel viewModelBuilder(BuildContext context) => + RegisterViewModel(); + + @override + Widget builder( + BuildContext context, + RegisterViewModel viewModel, + Widget? child, + ) => + _buildRegisterScreensWrapper(viewModel); + + Widget _buildRegisterScreensWrapper(RegisterViewModel viewModel) => PopScope( + canPop: false, + onPopInvokedWithResult: (value, data) { + if (value) return; + WidgetsBinding.instance.addPostFrameCallback((_) => viewModel.goBack()); + }, + child: _buildScaffoldWrapper(viewModel)); + + Widget _buildScaffoldWrapper(RegisterViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffoldStack(viewModel), + ); + + Widget _buildScaffoldStack(RegisterViewModel viewModel) => Stack( + children: [_buildScaffold(viewModel), _buildBusyRegistration(viewModel)]); + + Widget _buildScaffold(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildScaffoldChildren(viewModel), + ); + + List _buildScaffoldChildren(RegisterViewModel viewModel) => + [_buildAppBar(viewModel), _buildExpandedBody(viewModel)]; + + Widget _buildAppBar(RegisterViewModel viewModel) => LargeAppBar( + showBackButton: true, + onPop: viewModel.goBack, + showLanguageSelection: true, + ); + + Widget _buildExpandedBody(RegisterViewModel viewModel) => + Expanded(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(RegisterViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBody(viewModel), + ); + + Widget _buildBody(RegisterViewModel viewModel) => + IndexedStack(index: viewModel.currentIndex, children: _buildScreens()); + + List _buildScreens() => [ + _buildRegisterWithEmailScreen(), + _buildRegisterWithPhoneScreen(), + _buildCreatePasswordScreen(), + _buildRegistrationOtpScreen(), + ]; + + Widget _buildRegisterWithEmailScreen() => + RegisterWithEmailScreen(emailController: emailController); + + Widget _buildRegisterWithPhoneScreen() => RegisterWithPhoneNumberScreen( + phoneNumberController: phoneNumberController); + + Widget _buildRegistrationOtpScreen() => RegistrationOtpScreen( + otpController: otpController, + emailController: emailController, + phoneNumberController: phoneNumberController, + ); + + Widget _buildCreatePasswordScreen() => CreatePasswordScreen( + passwordController: passwordController, + confirmPasswordController: confirmPasswordController); + + Widget _buildBusyRegistration(RegisterViewModel viewModel) => + viewModel.isBusy ? const PageLoadingIndicator() : Container(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart new file mode 100644 index 0000000..222853d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_view.form.dart @@ -0,0 +1,308 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// StackedFormGenerator +// ************************************************************************** + +// ignore_for_file: public_member_api_docs, constant_identifier_names, non_constant_identifier_names,unnecessary_this + +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/validators/form_validator.dart'; + +const bool _autoTextFieldValidation = true; + +const String OtpValueKey = 'otp'; +const String EmailValueKey = 'email'; +const String PasswordValueKey = 'password'; +const String PhoneNumberValueKey = 'phoneNumber'; +const String ConfirmPasswordValueKey = 'confirmPassword'; + +final Map _RegisterViewTextEditingControllers = + {}; + +final Map _RegisterViewFocusNodes = {}; + +final Map _RegisterViewTextValidations = { + OtpValueKey: FormValidator.validateForm, + EmailValueKey: FormValidator.validateEmail, + PasswordValueKey: FormValidator.validateForm, + PhoneNumberValueKey: FormValidator.validateForm, + ConfirmPasswordValueKey: FormValidator.validateForm, +}; + +mixin $RegisterView { + TextEditingController get otpController => + _getFormTextEditingController(OtpValueKey); + TextEditingController get emailController => + _getFormTextEditingController(EmailValueKey); + TextEditingController get passwordController => + _getFormTextEditingController(PasswordValueKey); + TextEditingController get phoneNumberController => + _getFormTextEditingController(PhoneNumberValueKey); + TextEditingController get confirmPasswordController => + _getFormTextEditingController(ConfirmPasswordValueKey); + + FocusNode get otpFocusNode => _getFormFocusNode(OtpValueKey); + FocusNode get emailFocusNode => _getFormFocusNode(EmailValueKey); + FocusNode get passwordFocusNode => _getFormFocusNode(PasswordValueKey); + FocusNode get phoneNumberFocusNode => _getFormFocusNode(PhoneNumberValueKey); + FocusNode get confirmPasswordFocusNode => + _getFormFocusNode(ConfirmPasswordValueKey); + + TextEditingController _getFormTextEditingController( + String key, { + String? initialValue, + }) { + if (_RegisterViewTextEditingControllers.containsKey(key)) { + return _RegisterViewTextEditingControllers[key]!; + } + + _RegisterViewTextEditingControllers[key] = + TextEditingController(text: initialValue); + return _RegisterViewTextEditingControllers[key]!; + } + + FocusNode _getFormFocusNode(String key) { + if (_RegisterViewFocusNodes.containsKey(key)) { + return _RegisterViewFocusNodes[key]!; + } + _RegisterViewFocusNodes[key] = FocusNode(); + return _RegisterViewFocusNodes[key]!; + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + void syncFormWithViewModel(FormStateHelper model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Registers a listener on every generated controller that calls [model.setData()] + /// with the latest textController values + @Deprecated( + 'Use syncFormWithViewModel instead.' + 'This feature was deprecated after 3.1.0.', + ) + void listenToFormUpdated(FormViewModel model) { + otpController.addListener(() => _updateFormData(model)); + emailController.addListener(() => _updateFormData(model)); + passwordController.addListener(() => _updateFormData(model)); + phoneNumberController.addListener(() => _updateFormData(model)); + confirmPasswordController.addListener(() => _updateFormData(model)); + + _updateFormData(model, forceValidate: _autoTextFieldValidation); + } + + /// Updates the formData on the FormViewModel + void _updateFormData(FormStateHelper model, {bool forceValidate = false}) { + model.setData( + model.formValueMap + ..addAll({ + OtpValueKey: otpController.text, + EmailValueKey: emailController.text, + PasswordValueKey: passwordController.text, + PhoneNumberValueKey: phoneNumberController.text, + ConfirmPasswordValueKey: confirmPasswordController.text, + }), + ); + + if (_autoTextFieldValidation || forceValidate) { + updateValidationData(model); + } + } + + bool validateFormFields(FormViewModel model) { + _updateFormData(model, forceValidate: true); + return model.isFormValid; + } + + /// Calls dispose on all the generated controllers and focus nodes + void disposeForm() { + // The dispose function for a TextEditingController sets all listeners to null + + for (var controller in _RegisterViewTextEditingControllers.values) { + controller.dispose(); + } + for (var focusNode in _RegisterViewFocusNodes.values) { + focusNode.dispose(); + } + + _RegisterViewTextEditingControllers.clear(); + _RegisterViewFocusNodes.clear(); + } +} + +extension ValueProperties on FormStateHelper { + bool get hasAnyValidationMessage => this + .fieldsValidationMessages + .values + .any((validation) => validation != null); + + bool get isFormValid { + if (!_autoTextFieldValidation) this.validateForm(); + + return !hasAnyValidationMessage; + } + + String? get otpValue => this.formValueMap[OtpValueKey] as String?; + String? get emailValue => this.formValueMap[EmailValueKey] as String?; + String? get passwordValue => this.formValueMap[PasswordValueKey] as String?; + String? get phoneNumberValue => + this.formValueMap[PhoneNumberValueKey] as String?; + String? get confirmPasswordValue => + this.formValueMap[ConfirmPasswordValueKey] as String?; + + set otpValue(String? value) { + this.setData( + this.formValueMap..addAll({OtpValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(OtpValueKey)) { + _RegisterViewTextEditingControllers[OtpValueKey]?.text = value ?? ''; + } + } + + set emailValue(String? value) { + this.setData( + this.formValueMap..addAll({EmailValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(EmailValueKey)) { + _RegisterViewTextEditingControllers[EmailValueKey]?.text = value ?? ''; + } + } + + set passwordValue(String? value) { + this.setData( + this.formValueMap..addAll({PasswordValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(PasswordValueKey)) { + _RegisterViewTextEditingControllers[PasswordValueKey]?.text = value ?? ''; + } + } + + set phoneNumberValue(String? value) { + this.setData( + this.formValueMap..addAll({PhoneNumberValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey(PhoneNumberValueKey)) { + _RegisterViewTextEditingControllers[PhoneNumberValueKey]?.text = + value ?? ''; + } + } + + set confirmPasswordValue(String? value) { + this.setData( + this.formValueMap..addAll({ConfirmPasswordValueKey: value}), + ); + + if (_RegisterViewTextEditingControllers.containsKey( + ConfirmPasswordValueKey)) { + _RegisterViewTextEditingControllers[ConfirmPasswordValueKey]?.text = + value ?? ''; + } + } + + bool get hasOtp => + this.formValueMap.containsKey(OtpValueKey) && + (otpValue?.isNotEmpty ?? false); + bool get hasEmail => + this.formValueMap.containsKey(EmailValueKey) && + (emailValue?.isNotEmpty ?? false); + bool get hasPassword => + this.formValueMap.containsKey(PasswordValueKey) && + (passwordValue?.isNotEmpty ?? false); + bool get hasPhoneNumber => + this.formValueMap.containsKey(PhoneNumberValueKey) && + (phoneNumberValue?.isNotEmpty ?? false); + bool get hasConfirmPassword => + this.formValueMap.containsKey(ConfirmPasswordValueKey) && + (confirmPasswordValue?.isNotEmpty ?? false); + + bool get hasOtpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]?.isNotEmpty ?? false; + bool get hasEmailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]?.isNotEmpty ?? false; + bool get hasPasswordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]?.isNotEmpty ?? false; + bool get hasPhoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]?.isNotEmpty ?? false; + bool get hasConfirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]?.isNotEmpty ?? + false; + + String? get otpValidationMessage => + this.fieldsValidationMessages[OtpValueKey]; + String? get emailValidationMessage => + this.fieldsValidationMessages[EmailValueKey]; + String? get passwordValidationMessage => + this.fieldsValidationMessages[PasswordValueKey]; + String? get phoneNumberValidationMessage => + this.fieldsValidationMessages[PhoneNumberValueKey]; + String? get confirmPasswordValidationMessage => + this.fieldsValidationMessages[ConfirmPasswordValueKey]; +} + +extension Methods on FormStateHelper { + setOtpValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[OtpValueKey] = validationMessage; + setEmailValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[EmailValueKey] = validationMessage; + setPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PasswordValueKey] = validationMessage; + setPhoneNumberValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[PhoneNumberValueKey] = validationMessage; + setConfirmPasswordValidationMessage(String? validationMessage) => + this.fieldsValidationMessages[ConfirmPasswordValueKey] = + validationMessage; + + /// Clears text input fields on the Form + void clearForm() { + otpValue = ''; + emailValue = ''; + passwordValue = ''; + phoneNumberValue = ''; + confirmPasswordValue = ''; + } + + /// Validates text input fields on the Form + void validateForm() { + this.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); + } +} + +/// Returns the validation message for the given key +String? getValidationMessage(String key) { + final validatorForKey = _RegisterViewTextValidations[key]; + if (validatorForKey == null) return null; + + String? validationMessageForKey = validatorForKey( + _RegisterViewTextEditingControllers[key]!.text, + ); + + return validationMessageForKey; +} + +/// Updates the fieldsValidationMessages on the FormViewModel +void updateValidationData(FormStateHelper model) => + model.setValidationMessages({ + OtpValueKey: getValidationMessage(OtpValueKey), + EmailValueKey: getValidationMessage(EmailValueKey), + PasswordValueKey: getValidationMessage(PasswordValueKey), + PhoneNumberValueKey: getValidationMessage(PhoneNumberValueKey), + ConfirmPasswordValueKey: getValidationMessage(ConfirmPasswordValueKey), + }); diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart new file mode 100644 index 0000000..be38d68 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/register_viewmodel.dart @@ -0,0 +1,310 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/home/home_view.dart'; +import 'package:yimaru_app/ui/views/login/login_view.dart'; + +import '../../../app/app.locator.dart'; +import '../../../models/user_model.dart'; + +class RegisterViewModel extends FormViewModel { + final _apiService = locator(); + final _navigationService = locator(); + final _authenticationService = locator(); + + // Navigation + int _currentIndex = 0; + + int get currentIndex => _currentIndex; + + // Email + bool _focusEmail = false; + + bool get focusEmail => _focusEmail; + + // Password + bool _length = false; + + bool get length => _length; + + bool _number = false; + + bool get number => _number; + + bool _specialChar = false; + + bool get specialChar => _specialChar; + + bool _focusPassword = false; + + bool get focusPassword => _focusPassword; + + bool _obscurePassword = true; + + bool get obscurePassword => _obscurePassword; + + bool _passwordMatch = false; + + bool get passwordMatch => _passwordMatch; + + // Confirm password + bool _focusConfirmPassword = false; + + bool get focusConfirmPassword => _focusConfirmPassword; + + bool _obscureConfirmPassword = true; + + bool get obscureConfirmPassword => _obscureConfirmPassword; + + // Phone number + bool _focusPhoneNumber = false; + + bool get focusPhoneNumber => _focusPhoneNumber; + + // Terms and conditions + bool _agree = false; + + bool get agree => _agree; + + // Focus otp + bool _focusOtp = false; + + bool get focusOtp => _focusOtp; + + // Focus node + final FocusNode _focusNode = FocusNode(); + + FocusNode get focusNode => _focusNode; + + // Registration type + RegistrationType? _registrationType; + + RegistrationType? get registrationType => _registrationType; + + // Resend button state + bool _buttonActive = false; + + bool get buttonActive => _buttonActive; + + DateTime _resendTime = + DateTime.now().add(const Duration(minutes: 3, seconds: 0)); + + DateTime get resendTime => _resendTime; + + // User data + final Map _userData = {}; + + Map get userData => _userData; + + // Email + void setEmailFocus() { + _focusEmail = true; + rebuildUi(); + } + + // Password + void setPasswordFocus() { + _focusPassword = true; + rebuildUi(); + } + + void validatePassword( + {required String password, required String confirmPassword}) { + if (password.length > 8) { + _length = true; + } else { + _length = false; + } + + if (RegExp(r'\d').hasMatch(password)) { + _number = true; + } else { + _number = false; + } + + if (RegExp(r'[!@#$%^&*(),.?":{}|<>]').hasMatch(password)) { + _specialChar = true; + } else { + _specialChar = false; + } + + if (password == confirmPassword) { + _passwordMatch = true; + } else { + _passwordMatch = false; + } + rebuildUi(); + } + + double validationProgress() { + int completed = 0; + + if (_length) completed++; + if (_number) completed++; + if (_specialChar) completed++; + if (_passwordMatch) completed++; + + return completed / 4; // returns 0.0 → 1.0 + } + + void setObscurePassword() { + _obscurePassword = !_obscurePassword; + rebuildUi(); + } + + // Confirm password + void setConfirmPasswordFocus() { + _focusConfirmPassword = true; + rebuildUi(); + } + + void setObscureConfirmPassword() { + _obscureConfirmPassword = !_obscureConfirmPassword; + rebuildUi(); + } + + // Phone number + void setPhoneNumberFocus() { + _focusPhoneNumber = true; + rebuildUi(); + } + + // Otp + void setOtpFocus() { + _focusOtp = true; + rebuildUi(); + } + + // Terms and Conditions + void setAgreement(bool value) { + _agree = value; + rebuildUi(); + } + + void setResendButton() { + _buttonActive = true; + rebuildUi(); + } + + void resetButton() { + _buttonActive = false; + + _resendTime = DateTime.now().add(const Duration(minutes: 3, seconds: 0)); + } + + // Validate otp + Future validateOtp(String otp) async {} + + // Add user data + void addUserData(Map data) { + _userData.addAll(data); + } + + void clearUserData() { + _userData.clear(); + } + + // Remote api calls + Future register() async { + Map response = await runBusyFuture>( + _apiService.register(_userData)); + + if (response['status'] == ResponseStatus.success) { + goTo(page: 3); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + + Future verifyOtp() async { + Map response = + await runBusyFuture>(_verifyOtp()); + + if (response['status'] == ResponseStatus.success) { + await replaceWithHome(); + } + } + + Future> _verifyOtp() async { + Map response = await _apiService.verifyOtp(_userData); + if (response['status'] == ResponseStatus.success) { + // UserModel user = response['data'] as UserModel; + // Map data = { + // 'userId': user.userId, + // 'accessToken': user.accessToken, + // 'refreshToken': user.refreshToken + // }; + + await _authenticationService.saveUserData({ + 'userId': 10, + 'accessToken': 'accessToken', + 'refreshToken': 'refreshToken' + }); + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + + return response; + } + + Future resendOtp() async { + resetButton(); + + Map response = await runBusyFuture>( + _apiService.resendOtp(_userData)); + + if (response['status'] == ResponseStatus.success) { + showSuccessToast(response['message']); + } else { + showErrorToast(response['message']); + } + } + + // Navigation + void goTo({required int page, RegistrationType? type}) { + _currentIndex = page; + if (type != null) { + _registrationType = type; + } + rebuildUi(); + } + + void goBack() { + if (_currentIndex == 1) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 2) { + _currentIndex = 0; + rebuildUi(); + } else if (_currentIndex == 3) { + if (_registrationType == RegistrationType.phone) { + _currentIndex = 1; + } else { + _currentIndex = 2; + } + + rebuildUi(); + } else { + _navigationService.back(); + } + } + + Future navigateToTermsAndConditions() async => + await _navigationService.navigateToTermsAndConditionsView(); + + Future navigateToPrivacyPolicy() async => + await _navigationService.navigateToPrivacyPolicyView(); + + Future replaceToLogin() async => + await _navigationService.replaceWithLoginView(); + + Future replaceWithHome() async => + await _navigationService.clearStackAndShowView(const HomeView()); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart new file mode 100644 index 0000000..87aaa6e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/create_password_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_form_label.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; +import 'package:yimaru_app/ui/widgets/validator_list_tile.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/obscure_password.dart'; +import '../register_view.form.dart'; + +class CreatePasswordScreen extends ViewModelWidget { + final TextEditingController passwordController; + final TextEditingController confirmPasswordController; + + const CreatePasswordScreen( + {super.key, + required this.passwordController, + required this.confirmPasswordController}); + + Future _signUp(RegisterViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'role': 'STUDENT', + 'otp_medium': 'email', + 'password': passwordController.text, + }; + viewModel.addUserData(data); + + await viewModel.register(); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBodyChildren(viewModel); + + Widget _buildBodyChildren(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildBodyColumn(viewModel), + ); + + Widget _buildBodyColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyColumnChildren(viewModel), + ); + + List _buildBodyColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildPasswordLabel('Password'), + verticalSpaceSmall, + _buildPasswordFormField(viewModel), + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + verticalSpaceTiny, + if (viewModel.hasPasswordValidationMessage && viewModel.focusPassword) + _buildPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildPasswordLabel('Confirm Password'), + verticalSpaceSmall, + _buildConfirmPasswordFormField(viewModel), + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + verticalSpaceTiny, + if (viewModel.hasConfirmPasswordValidationMessage && + viewModel.focusConfirmPassword) + _buildConfirmPasswordValidationWrapper(viewModel), + verticalSpaceMedium, + _buildLinearProgressIndicator(viewModel), + verticalSpaceSmall, + _buildCharLengthValidator(viewModel), + _buildNumberValidator(viewModel), + _buildSymbolValidator(viewModel), + _buildPasswordMatchValidator(viewModel), + _buildCheckBox(viewModel), + verticalSpaceSmall, + _buildSignUpButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildTitle() => const Text( + 'Create Password', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildPasswordLabel(String label) => CustomFormLabel( + label: label, + style: style14DG400, + ); + + Widget _buildPasswordFormField(RegisterViewModel viewModel) => TextFormField( + controller: passwordController, + onTap: viewModel.setPasswordFocus, + obscureText: viewModel.obscurePassword, + decoration: inputDecoration( + hint: 'Password', + focus: viewModel.focusPassword, + suffix: _buildObscurePassword(viewModel), + filled: passwordController.text.isNotEmpty), + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + ); + + Widget _buildObscurePassword(RegisterViewModel viewModel) => ObscurePassword( + focus: viewModel.focusPassword, + obscure: viewModel.obscurePassword, + onTap: viewModel.setObscurePassword, + ); + + Widget _buildPasswordValidationWrapper(RegisterViewModel viewModel) => + viewModel.hasPasswordValidationMessage + ? _buildPasswordValidator(viewModel) + : Container(); + + Widget _buildPasswordValidator(RegisterViewModel viewModel) => Text( + viewModel.passwordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildConfirmPasswordFormField(RegisterViewModel viewModel) => + TextFormField( + controller: confirmPasswordController, + onTap: viewModel.setConfirmPasswordFocus, + obscureText: viewModel.obscureConfirmPassword, + onChanged: (value) => viewModel.validatePassword( + password: passwordController.text, + confirmPassword: confirmPasswordController.text), + decoration: inputDecoration( + hint: 'Confirm Password', + focus: viewModel.focusConfirmPassword, + suffix: _buildObscureConfirmPassword(viewModel), + filled: confirmPasswordController.text.isNotEmpty), + ); + + Widget _buildObscureConfirmPassword(RegisterViewModel viewModel) => + ObscurePassword( + focus: viewModel.focusConfirmPassword, + obscure: viewModel.obscureConfirmPassword, + onTap: viewModel.setObscureConfirmPassword, + ); + + Widget _buildConfirmPasswordValidationWrapper(RegisterViewModel viewModel) => + viewModel.hasConfirmPasswordValidationMessage + ? _buildConfirmPasswordValidator(viewModel) + : Container(); + + Widget _buildConfirmPasswordValidator(RegisterViewModel viewModel) => Text( + viewModel.confirmPasswordValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLinearProgressIndicator(RegisterViewModel viewModel) => + CustomLinearProgressIndicator( + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + progress: viewModel.validationProgress(), + ); + + Widget _buildCharLengthValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.length ? kcPrimaryColor : kcLightGrey, + label: '8 characters minimum'); + + Widget _buildNumberValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.number ? kcPrimaryColor : kcLightGrey, + label: 'a number'); + + Widget _buildSymbolValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: viewModel.specialChar ? kcPrimaryColor : kcLightGrey, + label: 'one symbol minimum'); + + Widget _buildPasswordMatchValidator(RegisterViewModel viewModel) => + ValidatorListTile( + backgroundColor: + viewModel.passwordMatch ? kcPrimaryColor : kcLightGrey, + label: 'password match'); + + Widget _buildCheckBox(RegisterViewModel viewMode) => CheckboxListTile( + value: viewMode.agree, + activeColor: kcPrimaryColor, + title: _buildCheckBoxTitle(viewMode), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) => viewMode.setAgreement(value ?? false)); + + Widget _buildCheckBoxTitle(RegisterViewModel viewMode) => Text.rich( + TextSpan( + text: 'By clicking "Sign Up", you agree to our', + style: style14DG400, + children: [ + TextSpan( + text: ' Terms of Service', + style: style14P600, + recognizer: TapGestureRecognizer() + ..onTap = () => viewMode.navigateToTermsAndConditions()), + TextSpan(text: ' and ', style: style14DG400), + TextSpan( + text: 'Privacy Policy', + style: style14P600, + recognizer: TapGestureRecognizer() + ..onTap = () => viewMode.navigateToPrivacyPolicy()), + ]), + ); + + Widget _buildSignUpButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Sign Up', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + (viewModel.focusPassword && passwordController.text.isNotEmpty) && + (viewModel.focusConfirmPassword && + confirmPasswordController.text.isNotEmpty) && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch && + viewModel.agree + ? () async => await _signUp(viewModel) + : null, + backgroundColor: + (viewModel.focusPassword && passwordController.text.isNotEmpty) && + (viewModel.focusConfirmPassword && + confirmPasswordController.text.isNotEmpty) && + viewModel.number && + viewModel.length && + viewModel.specialChar && + viewModel.specialChar && + viewModel.passwordMatch && + viewModel.agree + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart new file mode 100644 index 0000000..6b35e65 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_email_screen.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/widgets/login_account.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/option_text_divider.dart'; +import '../register_viewmodel.dart'; +import '../register_view.form.dart'; + +class RegisterWithEmailScreen extends ViewModelWidget { + final TextEditingController emailController; + + const RegisterWithEmailScreen({ + super.key, + required this.emailController, + }); + + void _addUserData(RegisterViewModel viewModel) { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'email': emailController.text, + }; + viewModel.addUserData(data); + viewModel.goTo(page: 2, type: RegistrationType.email); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceLarge, + _buildEmailFormField(viewModel), + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + verticalSpaceTiny, + if (viewModel.hasEmailValidationMessage && viewModel.focusEmail) + _buildEmailValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Create an Account', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + onTap: () async => await viewModel.replaceToLogin(), + ); + + Widget _buildEmailFormField(RegisterViewModel viewModel) => TextFormField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + onTap: viewModel.setEmailFocus, + decoration: inputDecoration( + hint: 'Email', + focus: viewModel.focusEmail, + filled: emailController.text.isNotEmpty), + ); + + Widget _buildEmailValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasEmailValidationMessage + ? _buildEmailValidator(viewModel) + : Container(); + + Widget _buildEmailValidator(RegisterViewModel viewModel) => Text( + viewModel.emailValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(RegisterViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildRegisterWithEmailButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: viewModel.focusEmail && + emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? () => _addUserData(viewModel) + : null, + backgroundColor: viewModel.focusEmail && + emailController.text.isNotEmpty && + !viewModel.hasEmailValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildRegisterWithEmailButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.phone, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + text: 'Register with Phone Number', + onTap: () => viewModel.goTo(page: 1), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart new file mode 100644 index 0000000..77e3e58 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/register_with_phone_number_screen.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/login_account.dart'; +import 'package:yimaru_app/ui/widgets/option_text_divider.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/enmus.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; +import '../../../widgets/phone_number_prefix.dart'; +import '../register_viewmodel.dart'; +import '../register_view.form.dart'; + +class RegisterWithPhoneNumberScreen extends ViewModelWidget { + final TextEditingController phoneNumberController; + const RegisterWithPhoneNumberScreen( + {super.key, required this.phoneNumberController}); + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + _buildSubTitleWrapper(viewModel), + verticalSpaceMedium, + _buildSubtitle(), + verticalSpaceMedium, + _buildPhoneNumberWrapper(viewModel), + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + verticalSpaceTiny, + if (viewModel.hasPhoneNumberValidationMessage && + viewModel.focusPhoneNumber) + _buildPhoneNumberValidatorWrapper(viewModel), + ]; + + Widget _buildTitle() => const Text( + 'Create an Account', + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitleWrapper(RegisterViewModel viewModel) => LoginAccount( + onTap: () async => await viewModel.replaceToLogin(), + ); + + Widget _buildSubtitle() => const Text( + 'Enter your phone number. We will send you a confirmation code there', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildPhoneNumberWrapper(RegisterViewModel viewModel) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildPhoneNumberChildren(viewModel), + ); + + List _buildPhoneNumberChildren(RegisterViewModel viewModel) => [ + _buildPhoneNumberPrefix(viewModel), + horizontalSpaceSmall, + _buildPhoneNumberFormFieldWrapper(viewModel), + ]; + + Widget _buildPhoneNumberPrefix(RegisterViewModel viewModel) => + PhoneNumberPrefix(selected: viewModel.focusPhoneNumber); + + Widget _buildPhoneNumberFormFieldWrapper(RegisterViewModel viewModel) => + Expanded(child: _buildPhoneNumberFormField(viewModel)); + + Widget _buildPhoneNumberFormField(RegisterViewModel viewModel) => + TextFormField( + maxLength: 9, + keyboardType: TextInputType.phone, + controller: phoneNumberController, + onTap: viewModel.setPhoneNumberFocus, + decoration: inputDecoration( + focus: viewModel.focusPhoneNumber, + filled: phoneNumberController.text.isNotEmpty), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ); + + Widget _buildPhoneNumberValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasPhoneNumberValidationMessage + ? _buildPhoneNumberValidator(viewModel) + : Container(); + + Widget _buildPhoneNumberValidator(RegisterViewModel viewModel) => Text( + viewModel.phoneNumberValidationMessage!, + style: const TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w700, + ), + ); + + Widget _buildLowerColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(RegisterViewModel viewModel) => [ + _buildContinueButton(viewModel), + _buildOptionTextDivider(), + _buildRegisterWitPhoneNumberButton(viewModel), + verticalSpaceMedium + ]; + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + onTap: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? () => viewModel.goTo(page: 3, type: RegistrationType.phone) + : null, + backgroundColor: + viewModel.focusPhoneNumber && phoneNumberController.text.isNotEmpty + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + ); + + Widget _buildRegisterWitPhoneNumberButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + borderRadius: 12, + backgroundColor: kcWhite, + leadingIcon: Icons.email, + borderColor: kcPrimaryColor, + text: 'Register with Email', + foregroundColor: kcPrimaryColor, + onTap: () => viewModel.goTo(page: 0), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart b/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart new file mode 100644 index 0000000..9c822ee --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/register/screens/registration_otp_screen.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_timer_countdown/flutter_timer_countdown.dart'; +import 'package:pinput/pinput.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; + +import 'package:yimaru_app/ui/views/register/register_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_cursor.dart'; + +import '../../../common/app_colors.dart'; +import '../../../common/ui_helpers.dart'; +import '../../../widgets/custom_elevated_button.dart'; + +import '../register_view.form.dart'; + +class RegistrationOtpScreen extends ViewModelWidget { + final TextEditingController otpController; + final TextEditingController emailController; + final TextEditingController phoneNumberController; + + const RegistrationOtpScreen( + {super.key, + required this.otpController, + required this.emailController, + required this.phoneNumberController}); + + Future _verifyOtp(RegisterViewModel viewModel) async { + FocusManager.instance.primaryFocus?.unfocus(); + + Map data = { + 'otp': otpController.text, + 'email': emailController.text, + }; + viewModel.clearUserData(); + viewModel.addUserData(data); + + await viewModel.verifyOtp(); + } + + @override + Widget build(BuildContext context, RegisterViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(RegisterViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(RegisterViewModel viewModel) => + [_buildColumnScroller(viewModel), _buildContinueButtonWrapper(viewModel)]; + + Widget _buildColumnScroller(RegisterViewModel viewModel) => + SingleChildScrollView( + child: _buildUpperColumn(viewModel), + ); + + Widget _buildUpperColumn(RegisterViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(RegisterViewModel viewModel) => [ + verticalSpaceMedium, + _buildTitle(), + verticalSpaceMedium, + _buildSubtitleWrapper(), + verticalSpaceMedium, + _buildPinPutWrapper(viewModel), + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + verticalSpaceTiny, + if (viewModel.hasOtpValidationMessage && viewModel.focusOtp) + _buildOtpValidatorWrapper(viewModel), + verticalSpaceSmall, + _buildTimerWrapper(viewModel) + ]; + + Widget _buildTitle() => Text( + 'Verification Code', + style: style25DG600, + ); + + Widget _buildSubtitleWrapper() => + phoneNumberController.text.length == 9 ? _buildSubtitle() : Container(); + + Widget _buildSubtitle() => Text( + 'Code sent to your number +251${phoneNumberController.text.substring(0, 5)}****', + style: style14DG400, + ); + + Widget _buildPinPutWrapper(RegisterViewModel viewModel) => Center( + child: _buildPinPut(viewModel), + ); + + Widget _buildPinPut(RegisterViewModel viewModel) => Pinput( + length: 6, + controller: otpController, + defaultPinTheme: defaultPin, + cursor: const CustomCursor(), + errorPinTheme: errorPinTheme, + onTap: viewModel.setOtpFocus, + focusNode: viewModel.focusNode, + errorTextStyle: validationStyle, + //smsRetriever: locator(), + focusedPinTheme: focusedThemePin, + submittedPinTheme: submittedThemePin, + hapticFeedbackType: HapticFeedbackType.heavyImpact, + separatorBuilder: (index) => const SizedBox(width: 10), + onCompleted: (otp) async => await _verifyOtp(viewModel), + ); + + Widget _buildOtpValidatorWrapper(RegisterViewModel viewModel) => + viewModel.hasOtpValidationMessage + ? _buildOtpValidator(viewModel) + : Container(); + + Widget _buildOtpValidator(RegisterViewModel viewModel) => Text( + viewModel.otpValidationMessage!, + style: style12R700, + ); + + Widget _buildTimerWrapper(RegisterViewModel viewModel) => + !viewModel.buttonActive + ? _buildTimerSection(viewModel) + : _buildResendButton(viewModel); + + Widget _buildResendButton(RegisterViewModel viewModel) => TextButton( + onPressed: () async => await viewModel.resendOtp(), + child: _buildResendText()); + + Widget _buildResendText() => Text( + 'Resend code', + style: style14P600.copyWith(fontStyle: FontStyle.italic), + ); + + Widget _buildTimerSection(RegisterViewModel viewModel) => Row( + children: [ + _buildCountdownText(), + horizontalSpaceSmall, + _buildTimer(viewModel) + ], + ); + + Widget _buildCountdownText() => Text('Resend code in ', style: style14DG400); + + Widget _buildTimer(RegisterViewModel viewModel) => TimerCountdown( + enableDescriptions: false, + timeTextStyle: style14P600, + endTime: viewModel.resendTime, + onEnd: viewModel.setResendButton, + format: CountDownTimerFormat.minutesSeconds, + colonsTextStyle: const TextStyle(color: kcPrimaryColor), + ); + + Widget _buildContinueButtonWrapper(RegisterViewModel viewModel) => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildContinueButton(viewModel), + ); + + Widget _buildContinueButton(RegisterViewModel viewModel) => + CustomElevatedButton( + height: 55, + text: 'Continue', + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: viewModel.focusOtp && + otpController.text.length == 6 && + !viewModel.hasOtpValidationMessage + ? kcPrimaryColor + : kcPrimaryColor.withOpacity(0.1), + onTap: viewModel.focusOtp && + otpController.text.length == 6 && + !viewModel.hasOtpValidationMessage + ? () async => await _verifyOtp(viewModel) + : null, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart index 42e039f..384a0ad 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stacked/stacked.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/custom_circular_progress_indicator.dart'; import '../../common/app_colors.dart'; import 'startup_viewmodel.dart'; @@ -47,7 +48,9 @@ class StartupView extends StackedView { ); List _buildUpperColumnChildren() => - [_buildIconWrapper(), _buildLoadingTextContainer()]; + [_buildIconWrapper(), _buildSafeWrapper()]; + + Widget _buildSafeWrapper() => SafeArea(child: _buildLoadingTextContainer()); Widget _buildLoadingTextContainer() => Padding( padding: const EdgeInsets.only(bottom: 50), @@ -66,8 +69,8 @@ class StartupView extends StackedView { _buildIndicatorWrapper(), ]; - Widget _buildLoadingText() => const Text('Loading ...', - style: TextStyle(color: kcWhiteColor, fontSize: 16)); + Widget _buildLoadingText() => + const Text('Loading ...', style: TextStyle(color: kcWhite, fontSize: 16)); Widget _buildIndicatorWrapper() => SizedBox( width: 16, @@ -75,10 +78,8 @@ class StartupView extends StackedView { child: _buildIndicator(), ); - Widget _buildIndicator() => const CircularProgressIndicator( - strokeWidth: 6, - color: kcWhiteColor, - ); + Widget _buildIndicator() => + const CustomCircularProgressIndicator(color: kcWhite); Widget _buildIconWrapper() => Padding( padding: const EdgeInsets.only(top: 100), @@ -87,6 +88,7 @@ class StartupView extends StackedView { Widget _buildIcon() => SvgPicture.asset( 'assets/icons/logo.svg', + height: 50, ); @override diff --git a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart index 1f4bc32..32b149a 100644 --- a/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart +++ b/StudioProjects/yimaru_app/lib/ui/views/startup/startup_viewmodel.dart @@ -1,19 +1,21 @@ import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; import '../../../app/app.locator.dart'; import '../../../app/app.router.dart'; class StartupViewModel extends BaseViewModel { final _navigationService = locator(); + final _authenticationService = locator(); // Place anything here that needs to happen before we get into the application Future runStartupLogic() async { - await Future.delayed(const Duration(seconds: 3)); - - // This is where you can make decisions on where your app should navigate when - // you have custom startup logic - - _navigationService.replaceWithOnboardingView(); + final response = await _authenticationService.userLoggedIn(); + if (response) { + _navigationService.replaceWithHomeView(); + } else { + _navigationService.replaceWithLoginView(); + } } } diff --git a/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart new file mode 100644 index 0000000..0010b9f --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/support/support_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/support_card.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/small_app_bar.dart'; +import 'support_viewmodel.dart'; + +class SupportView extends StackedView { + const SupportView({Key? key}) : super(key: key); + + @override + SupportViewModel viewModelBuilder(BuildContext context) => SupportViewModel(); + + @override + Widget builder( + BuildContext context, + SupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(SupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(SupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(SupportViewModel viewModel) => _buildBody(viewModel); + + Widget _buildBody(SupportViewModel viewModel) => _buildColumn(viewModel); + + Widget _buildColumn(SupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(SupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(SupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(SupportViewModel viewModel) => SmallAppBar( + title: 'Need Help?', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(SupportViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(SupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(SupportViewModel viewModel) => + SingleChildScrollView( + child: _buildMenuColumn(viewModel), + ); + + Widget _buildMenuColumn(SupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildMenuColumnChildren(viewModel), + ); + + List _buildMenuColumnChildren(SupportViewModel viewModel) => [ + verticalSpaceLarge, + _buildCallSupport(viewModel), + verticalSpaceMedium, + _buildTelegramSupport(viewModel) + ]; + + Widget _buildCallSupport(SupportViewModel viewModel) => SupportCard( + icon: Icons.call, + color: kcPrimaryColor, + title: 'Call Support', + subtitle: 'Talk with our support team directly', + onTap: () async => await viewModel.navigateToCallSupport(), + ); + + Widget _buildTelegramSupport(SupportViewModel viewModel) => SupportCard( + color: kcSkyBlue, + icon: Icons.telegram, + title: 'Telegram Support', + subtitle: 'Chat Instantly via Telegram', + onTap: () async => await viewModel.navigateToTelegramSupport(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart new file mode 100644 index 0000000..9691bf3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/support/support_viewmodel.dart @@ -0,0 +1,18 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/app/app.router.dart'; + +import '../../../app/app.locator.dart'; + +class SupportViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); + + Future navigateToTelegramSupport() async => + await _navigationService.navigateToTelegramSupportView(); + + Future navigateToCallSupport() async => + await _navigationService.navigateToCallSupportView(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart new file mode 100644 index 0000000..0d5507e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/widgets/circular_icon.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/option_text_divider.dart'; +import '../../widgets/small_app_bar.dart'; +import 'telegram_support_viewmodel.dart'; + +class TelegramSupportView extends StackedView { + const TelegramSupportView({Key? key}) : super(key: key); + + @override + TelegramSupportViewModel viewModelBuilder(BuildContext context) => + TelegramSupportViewModel(); + + @override + Widget builder( + BuildContext context, + TelegramSupportViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(TelegramSupportViewModel viewModel) => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(TelegramSupportViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(TelegramSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildBodyChildren(viewModel), + ); + + List _buildBodyChildren(TelegramSupportViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + _buildExpandedColumn(viewModel) + ]; + + Widget _buildAppBarWrapper(TelegramSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(TelegramSupportViewModel viewModel) => SmallAppBar( + title: 'Telegram Support', + onTap: viewModel.pop, + ); + + Widget _buildExpandedColumn(TelegramSupportViewModel viewModel) => + Expanded(child: _buildColumnWrapper(viewModel)); + + Widget _buildColumnWrapper(TelegramSupportViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(TelegramSupportViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(TelegramSupportViewModel viewModel) => + [_buildUpperColumn(viewModel), _buildLowerColumn(viewModel)]; + + Widget _buildUpperColumn(TelegramSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _buildUpperColumnChildren(viewModel), + ); + + List _buildUpperColumnChildren(TelegramSupportViewModel viewModel) => + [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceMedium, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + ]; + + Widget _buildIcon() => + const CircularIcon(icon: Icons.telegram, size: 50, color: kcSkyBlue); + + Widget _buildTitle() => const Text( + 'Join Yimaru Academy on Telegram', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => const Text( + 'Connect with our support team instantly on Telegram for quick assistance and community updates', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildLowerColumn(TelegramSupportViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + children: _buildLowerColumnChildren(viewModel), + ); + + List _buildLowerColumnChildren(TelegramSupportViewModel viewModel) => + [ + _buildContinueButton(viewModel), + verticalSpaceSmall, + _buildOptionTextDivider(), + verticalSpaceSmall, + _buildSearchText(), + verticalSpaceMedium + ]; + + Widget _buildContinueButton(TelegramSupportViewModel viewModel) => + const CustomElevatedButton( + height: 55, + borderRadius: 12, + leadingIcon: Icons.telegram, + text: 'Open in Telegram', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); + + Widget _buildOptionTextDivider() => const OptionTextDivider(); + + Widget _buildSearchText() => const Text.rich( + TextSpan( + text: 'Search for', + style: TextStyle( + color: kcDarkGrey, + ), + children: [ + TextSpan( + text: ' @YimaruSupport', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart new file mode 100644 index 0000000..c9f6120 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/telegram_support/telegram_support_viewmodel.dart @@ -0,0 +1,9 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class TelegramSupportViewModel extends BaseViewModel { + final _navigationService = locator(); + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart new file mode 100644 index 0000000..2b03944 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; + +import '../../common/app_colors.dart'; +import '../../common/ui_helpers.dart'; +import '../../widgets/custom_elevated_button.dart'; +import '../../widgets/small_app_bar.dart'; +import 'terms_and_conditions_viewmodel.dart'; + +class TermsAndConditionsView extends StackedView { + const TermsAndConditionsView({Key? key}) : super(key: key); + + @override + TermsAndConditionsViewModel viewModelBuilder(BuildContext context) => + TermsAndConditionsViewModel(); + + @override + Widget builder( + BuildContext context, + TermsAndConditionsViewModel viewModel, + Widget? child, + ) => + _buildScaffoldWrapper(viewModel); + + Widget _buildScaffoldWrapper(TermsAndConditionsViewModel viewModel) => + Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(viewModel), + ); + + Widget _buildScaffold(TermsAndConditionsViewModel viewModel) => + SafeArea(child: _buildBodyWrapper(viewModel)); + + Widget _buildBodyWrapper(TermsAndConditionsViewModel viewModel) => + _buildBody(viewModel); + + Widget _buildBody(TermsAndConditionsViewModel viewModel) => + _buildColumn(viewModel); + + Widget _buildColumn(TermsAndConditionsViewModel viewModel) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(TermsAndConditionsViewModel viewModel) => [ + verticalSpaceMedium, + _buildAppBarWrapper(viewModel), + verticalSpaceSmall, + _buildContentWrapper(viewModel) + ]; + + Widget _buildAppBarWrapper(TermsAndConditionsViewModel viewModel) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildAppbar(viewModel), + ); + + Widget _buildAppbar(TermsAndConditionsViewModel viewModel) => SmallAppBar( + title: 'Terms and Conditions', + onTap: viewModel.pop, + ); + + Widget _buildContentWrapper(TermsAndConditionsViewModel viewModel) => + Expanded(child: _buildContentColumnWrapper(viewModel)); + + Widget _buildContentColumnWrapper(TermsAndConditionsViewModel viewModel) => + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildMenuColumnScrollView(viewModel), + ); + + Widget _buildMenuColumnScrollView(TermsAndConditionsViewModel viewModel) => + SingleChildScrollView( + child: _buildContentColumn(), + ); + + Widget _buildContentColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildContentColumnChildren(), + ); + + List _buildContentColumnChildren() => [ + _buildContent(), + verticalSpaceMedium, + _buildDownloadButtonWrapper(), + ]; + + Widget _buildContent() => Html( + data: ksTerms, + shrinkWrap: true, + style: htmlStyle, + ); + + Widget _buildDownloadButtonWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 50), + child: _buildDownloadButton(), + ); + + Widget _buildDownloadButton() => const CustomElevatedButton( + height: 55, + borderRadius: 12, + text: 'Download PDF', + leadingIcon: Icons.download, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart new file mode 100644 index 0000000..589a058 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/views/terms_and_conditions/terms_and_conditions_viewmodel.dart @@ -0,0 +1,11 @@ +import 'package:stacked/stacked.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../../../app/app.locator.dart'; + +class TermsAndConditionsViewModel extends BaseViewModel { + final _navigationService = locator(); + + // Navigation + void pop() => _navigationService.back(); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart b/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart new file mode 100644 index 0000000..8aa15e8 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/birthday_selector.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/profile_detail/profile_detail_viewmodel.dart'; +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'package:omni_datetime_picker/omni_datetime_picker.dart'; + +class BirthdaySelector extends ViewModelWidget { + const BirthdaySelector({super.key}); + + DateTime _initialDate(ProfileDetailViewModel viewModel) { + try { + final parsedDate = format.parse(viewModel.selectedBirthday ?? ''); + return parsedDate.isAfter(DateTime.now()) ? DateTime.now() : parsedDate; + } catch (_) { + return DateTime.now(); + } + } + + Future _pickDateTime( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) async { + DateTime? dateTime = await showOmniDateTimePicker( + context: context, + is24HourMode: false, + isShowSeconds: false, + lastDate: DateTime.now(), + firstDate: DateTime(1900), + barrierDismissible: true, + titleSeparator: const Divider(), + padding: const EdgeInsets.all(16), + type: OmniDateTimePickerType.date, + initialDate: _initialDate(viewModel), + borderRadius: const BorderRadius.all(Radius.circular(15)), + insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), + title: const Text('Birthday', style: TextStyle(fontSize: 16)), + theme: ThemeData( + colorScheme: + const ColorScheme.light().copyWith(primary: kcPrimaryColor), + ), + ); + + if (dateTime != null) { + String formattedDateTime = DateFormat('d MMM, yyyy').format(dateTime); + + viewModel.setBirthday(DateFormat('d MMM, yyyy').format(dateTime)); + //onChanged(formattedDateTime); + } + } + + @override + Widget build(BuildContext context, ProfileDetailViewModel viewModel) => + _buildButtonWrapper(context: context, viewModel: viewModel); + + Widget _buildButtonWrapper( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) => + Container( + height: 50, + width: double.maxFinite, + margin: const EdgeInsets.only(bottom: 15), + child: _buildContainerWrapper(context: context, viewModel: viewModel), + ); + + Widget _buildContainerWrapper( + {required BuildContext context, + required ProfileDetailViewModel viewModel}) => + GestureDetector( + onTap: () async => + await _pickDateTime(context: context, viewModel: viewModel), + child: _buildContainer(viewModel), + ); + + Widget _buildContainer(ProfileDetailViewModel viewModel) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: kcPrimaryColor.withOpacity(0.1), + border: Border.all(color: kcPrimaryColor), + ), + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildButtonRowWrapper(viewModel), + ); + + Widget _buildButtonRowWrapper(ProfileDetailViewModel viewModel) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildButtonRowChildren(viewModel), + ); + + List _buildButtonRowChildren(ProfileDetailViewModel viewModel) => + [_buildText(viewModel), _buildIcon()]; + + Widget _buildText(ProfileDetailViewModel viewModel) => Text( + viewModel.selectedBirthday ?? 'Pick birthday', + style: const TextStyle(color: kcDarkGrey), + ); + + Widget _buildIcon() => const Icon( + Icons.calendar_month, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart b/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart new file mode 100644 index 0000000..f1241a3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/circular_icon.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class CircularIcon extends StatelessWidget { + final Color color; + final double size; + final IconData icon; + const CircularIcon( + {super.key, required this.icon, required this.size, required this.color}); + + @override + Widget build(BuildContext context) => _buildIconWrapper(); + + Widget _buildIconWrapper() => CircleAvatar( + radius: size, + backgroundColor: color.withOpacity(0.25), + child: _buildIcon(), + ); + + Widget _buildIcon() => Icon( + icon, + size: size, + color: color, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart b/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart new file mode 100644 index 0000000..3c5eb35 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/coming_soon.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ComingSoon extends StatelessWidget { + const ComingSoon({super.key}); + + @override + Widget build(BuildContext context) => _buildScaffoldWrapper(); + + Widget _buildScaffoldWrapper() => Scaffold( + backgroundColor: kcBackgroundColor, + body: _buildScaffold(), + ); + + Widget _buildScaffold() => Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: _buildBodyWrapper(), + ); + + Widget _buildBodyWrapper() => Center( + child: _buildBody(), + ); + + Widget _buildBody() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _buildBodyChildren(), + ); + + List _buildBodyChildren() => [ + verticalSpaceLarge, + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + ]; + + Widget _buildIcon() => Image.asset('assets/images/coming_soon.png'); + + Widget _buildTitle() => const Text( + 'Launching Very Soon!', + style: TextStyle( + fontSize: 22, + color: kcMediumGrey, + fontWeight: FontWeight.w700, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart new file mode 100644 index 0000000..b3e1074 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/course_level_card.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class CourseLevelCard extends StatelessWidget { + final Color color; + final String icon; + final String title; + final String status; + final String subTitle; + final bool isCompleted; + final GestureTapCallback? onTap; + + const CourseLevelCard({ + super.key, + this.onTap, + required this.icon, + required this.title, + required this.color, + required this.status, + required this.subTitle, + required this.isCompleted, + }); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + width: 200, + padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(left: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIconSection(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle(), + verticalSpaceSmall, + _buildActionButton() + ]; + + Widget _buildIconSection() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildIconSectionChildren(), + ); + + List _buildIconSectionChildren() => + [_buildIcon(), _buildProgressStatus()]; + + Widget _buildIcon() => SvgPicture.asset( + icon, + height: 50, + width: 50, + ); + + Widget _buildProgressStatus() => ProgressStatus( + color: color, + status: status, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => Expanded( + child: Text( + subTitle, + maxLines: 3, + style: const TextStyle(color: kcMediumGrey), + ), + ); + + Widget _buildActionButton() => CustomElevatedButton( + height: 15, + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + text: isCompleted ? 'Review Course' : 'Continue Learning', + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart b/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart new file mode 100644 index 0000000..ca54b1e --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/course_progress_section.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/ongoing_progress/ongoing_progress_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_column.dart'; +import 'custom_elevated_button.dart'; + +class CourseProgressSection extends ViewModelWidget { + const CourseProgressSection({super.key}); + + @override + Widget build(BuildContext context, OngoingProgressViewModel viewModel) => + _buildContainer(viewModel); + + Widget _buildContainer(OngoingProgressViewModel viewModel) => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(viewModel), + ); + + Widget _buildColumn(OngoingProgressViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(viewModel), + ); + + List _buildColumnChildren(OngoingProgressViewModel viewModel) => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle('Course'), + verticalSpaceMedium, + _buildExpansionTileWrapper(viewModel) + ]; + + Widget _buildIcon() => const Icon( + Icons.book, + size: 50, + color: kcPrimaryColor, + ); + + Widget _buildExpansionTileWrapper(OngoingProgressViewModel viewModel) => + Column( + mainAxisSize: MainAxisSize.min, + children: viewModel.courses + .map( + (course) => _buildExpansionTileCard(course['title']), + ) + .toList(), + ); + + Widget _buildExpansionTileCard(String title) => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildExpansionTile(title), + ); + + Widget _buildExpansionTile(String title) => ExpansionTile( + showTrailingIcon: true, + initiallyExpanded: true, + title: _buildTitle(title), + iconColor: kcDarkGrey, + textColor: kcDarkGrey, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + subtitle: _buildProgressIndicator(), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + shape: Border.all(color: kcPrimaryColor.withOpacity(0.2)), + children: _buildExpansionTileChildren(), + ); + + List _buildExpansionTileChildren() => [_buildTileWrapper()]; + + Widget _buildTitle(String title) => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildTileWrapper() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildTileChildren(), + ); + + List _buildTileChildren() => + [_buildLearningStatus(), verticalSpaceSmall, _buildActionButton()]; + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.5, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildLearningStatus() => Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: _buildLearningStatusChildren(), + ); + + List _buildLearningStatusChildren() => [ + horizontalSpaceMedium, + _buildWatchedVideos(), + horizontalSpaceSmall, + _buildCompletedPractices(), + horizontalSpaceMedium, + ]; + + Widget _buildWatchedVideos() => + const CustomColumn(title: '15/25', subtitle: 'Videos'); + + Widget _buildCompletedPractices() => + const CustomColumn(title: '8/12', subtitle: 'Practices'); + + Widget _buildActionButton() => const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'Continue Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart new file mode 100644 index 0000000..5e045fc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_back_button.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class CustomBackButton extends StatelessWidget { + final GestureTapCallback? onTap; + const CustomBackButton({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildBackButtonWrapper(); + + Widget _buildBackButtonWrapper() => GestureDetector( + onTap: onTap, + child: _buildBackIcon(), + ); + + Widget _buildBackIcon() => const Icon( + Icons.arrow_back, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart new file mode 100644 index 0000000..946ea16 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_circular_progress_indicator.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; + +class CustomCircularProgressIndicator extends StatelessWidget { + final Color color; + const CustomCircularProgressIndicator({super.key, required this.color}); + + @override + Widget build(BuildContext context) => _buildIndicator(); + + Widget _buildIndicator() => CircularProgressIndicator( + color: color, + strokeWidth: 6, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart new file mode 100644 index 0000000..bb7488d --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_column.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class CustomColumn extends StatelessWidget { + final String title; + final String subtitle; + const CustomColumn({super.key, required this.title, required this.subtitle}); + + @override + Widget build(BuildContext context) => _buildColumn(); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => + [_buildTitle(), verticalSpaceTiny, _buildSubtitle()]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + Widget _buildSubtitle() => Text( + subtitle, + maxLines: 1, + softWrap: false, + style: const TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart new file mode 100644 index 0000000..358e817 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_cursor.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class CustomCursor extends StatelessWidget { + const CustomCursor({super.key}); + + @override + Widget build(BuildContext context) => _buildCursor(); + + Widget _buildCursor() => Column( + mainAxisAlignment: MainAxisAlignment.end, + children: _buildPinIndicator(), + ); + + List _buildPinIndicator() => [ + Container( + width: 25, + height: 1, + color: kcPrimaryColor, + margin: const EdgeInsets.only(bottom: 9), + ), + ]; +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart index ec6edc2..be63f7c 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_dropdown.dart @@ -5,17 +5,17 @@ import 'package:flutter/material.dart'; import 'package:yimaru_app/ui/common/app_colors.dart'; import 'package:yimaru_app/ui/common/ui_helpers.dart'; -class CustomDropDownPicker extends StatelessWidget { - final Icon icon; +class CustomDropdownPicker extends StatelessWidget { + final Icon? icon; final String hint; final String selectedItem; final void Function(String?)? onChanged; final FutureOr> Function(String value, LoadProps? props)? items; - const CustomDropDownPicker( + const CustomDropdownPicker( {super.key, + this.icon, required this.hint, - required this.icon, required this.items, required this.onChanged, required this.selectedItem}); @@ -61,7 +61,6 @@ class CustomDropDownPicker extends StatelessWidget { InputDecoration _popUpDecoration() => InputDecoration( filled: true, errorBorder: searchBorder, - prefix: _buildPrefixIcon(), focusedBorder: searchBorder, enabledBorder: searchBorder, disabledBorder: searchBorder, @@ -70,7 +69,8 @@ class CustomDropDownPicker extends StatelessWidget { fontSize: 14, color: kcLightGrey, ), - fillColor: kcPrimaryColor.withOpacity(0.2), + fillColor: kcPrimaryColor.withOpacity(0.1), + prefix: icon != null ? _buildPrefixIcon() : null, contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 0), ); @@ -82,7 +82,7 @@ class CustomDropDownPicker extends StatelessWidget { Widget _buildPopupProsBuilder(String value) => Text( value, maxLines: 1, - style: const TextStyle(color: kcDarkGreyColor, fontSize: 14), + style: const TextStyle(color: kcDarkGrey, fontSize: 14), ); DropDownDecoratorProps _dropDownDecoratorProps() => DropDownDecoratorProps( @@ -103,7 +103,7 @@ class CustomDropDownPicker extends StatelessWidget { fontSize: 14, color: kcLightGrey, ), - fillColor: kcPrimaryColor.withOpacity(0.2), + fillColor: kcPrimaryColor.withOpacity(0.1), contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), ); @@ -121,7 +121,7 @@ class CustomDropDownPicker extends StatelessWidget { maxLines: 1, style: const TextStyle( fontSize: 14, - color: kcDarkGreyColor, + color: kcDarkGrey, ), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart index 113833a..b44e509 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_elevated_button.dart @@ -1,22 +1,30 @@ import 'package:flutter/material.dart'; class CustomElevatedButton extends StatelessWidget { - final bool icon; + final bool safe; final String text; final double width; final double height; final Color? borderColor; final double borderRadius; + final String? leadingImage; + final IconData? leadingIcon; final Color backgroundColor; final Color foregroundColor; + final String? trailingImage; + final IconData? trailingIcon; final GestureTapCallback? onTap; const CustomElevatedButton({ super.key, this.onTap, + this.leadingIcon, this.borderColor, - this.icon = false, + this.safe = true, + this.leadingImage, + this.trailingIcon, required this.text, + this.trailingImage, required this.height, this.borderRadius = 0, required this.backgroundColor, @@ -25,7 +33,10 @@ class CustomElevatedButton extends StatelessWidget { }); @override - Widget build(BuildContext context) => _buildButtonWrapper(); + Widget build(BuildContext context) => _buildSafeWrapper(); + + Widget _buildSafeWrapper() => + SafeArea(bottom: safe, child: _buildButtonWrapper()); Widget _buildButtonWrapper() => SizedBox(height: 50, width: width, child: _buildButton()); @@ -48,16 +59,39 @@ class CustomElevatedButton extends StatelessWidget { children: _buildRowChildren(), ); - List _buildRowChildren() => - [_buildText(), const SizedBox(width: 5), if (icon) _buildIcon()]; + List _buildRowChildren() => [ + if (leadingIcon != null) _buildIcon(leadingIcon!), + if (leadingImage != null) _buildImage(leadingImage!), + if (leadingIcon != null || leadingImage != null) + const SizedBox(width: 5), + leadingIcon == null && + trailingIcon == null && + leadingImage == null && + trailingImage == null + ? _buildExpandedText() + : _buildText(), + if (trailingIcon != null || trailingImage != null) + const SizedBox(width: 5), + if (trailingIcon != null) _buildIcon(trailingIcon!), + if (trailingImage != null) _buildImage(trailingImage!), + ]; - Widget _buildIcon() => Icon( - Icons.arrow_forward, + Widget _buildIcon(IconData icon) => Icon( + icon, color: foregroundColor, ); + Widget _buildImage(String image) => Image.asset(image); + + Widget _buildExpandedText() => Expanded( + child: _buildText(), + ); + Widget _buildText() => Text( text, + maxLines: 1, + softWrap: false, + textAlign: TextAlign.center, style: TextStyle(color: foregroundColor, fontWeight: FontWeight.bold), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart new file mode 100644 index 0000000..2b1142c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_form_label.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class CustomFormLabel extends StatelessWidget { + final String label; + final TextStyle style; + + const CustomFormLabel({super.key, required this.label, required this.style}); + + @override + Widget build(BuildContext context) => _buildLabel(); + + Widget _buildLabel() => Text( + label, + style: style, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart index d62b74f..8a7bbd7 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_large_radio_button.dart @@ -35,7 +35,7 @@ class CustomLargeRadioButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: selected ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, border: Border.all( color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), ), @@ -69,7 +69,7 @@ class CustomLargeRadioButton extends StatelessWidget { title, style: const TextStyle( fontSize: 18, - color: kcDarkGreyColor, + color: kcDarkGrey, fontWeight: FontWeight.w500, ), ); diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart new file mode 100644 index 0000000..6651618 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_linear_progress_indicator.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class CustomLinearProgressIndicator extends StatelessWidget { + final double progress; + final Color activeColor; + final Color backgroundColor; + + const CustomLinearProgressIndicator( + {super.key, + required this.progress, + required this.activeColor, + required this.backgroundColor}); + + @override + Widget build(BuildContext context) => _buildProgressIndicatorWrapper(); + + Widget _buildProgressIndicatorWrapper() => SizedBox( + height: 5, + width: double.maxFinite, + child: _buildProgressIndicatorClipper(), + ); + + Widget _buildProgressIndicatorClipper() => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildProgressIndicator(), + ); + + Widget _buildProgressIndicator() => LinearProgressIndicator( + value: progress, + color: activeColor, + backgroundColor: backgroundColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart new file mode 100644 index 0000000..c345915 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_list_tile.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class CustomListTile extends StatelessWidget { + final String title; + final IconData icon; + final String? language; + final bool? isLanguage; + final GestureTapCallback? onTap; + + const CustomListTile({ + super.key, + this.onTap, + this.language, + this.isLanguage, + required this.icon, + required this.title, + }); + + @override + Widget build(BuildContext context) => _buildLitTile(); + + Widget _buildLitTile() => ListTile( + onTap: onTap, + title: _buildTitle(), + leading: _buildLeading(), + trailing: _buildTrailing(), + ); + + Widget _buildLeading() => Icon( + icon, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildTrailing() => + isLanguage != null ? _buildTrailingRow() : _buildTrailingIcon(); + + Widget _buildTrailingRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTrailingChildren(), + ); + + List _buildTrailingChildren() => [ + if (language != null) _buildTrailingText(), + if (language != null) horizontalSpaceSmall, + _buildTrailingIcon() + ]; + + Widget _buildTrailingText() => Text( + language ?? '', + style: const TextStyle( + fontSize: 12, + color: kcDarkGrey, + ), + ); + + Widget _buildTrailingIcon() => const Icon( + Icons.keyboard_arrow_right, + color: kcLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart index aad8769..96fc29f 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/custom_small_radio_button.dart @@ -28,7 +28,7 @@ class CustomSmallRadioButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 15), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: selected ? kcPrimaryColor.withOpacity(0.2) : kcWhiteColor, + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, border: Border.all( color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), ), @@ -46,7 +46,7 @@ class CustomSmallRadioButton extends StatelessWidget { Widget _buildText() => Text( title, - style: const TextStyle(color: kcDarkGreyColor), + style: const TextStyle(color: kcDarkGrey), ); Widget _buildIcon() => const Icon( diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart new file mode 100644 index 0000000..76c0ef3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/download_card.dart @@ -0,0 +1,149 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; +import 'custom_elevated_button.dart'; + +class DownloadCard extends StatelessWidget { + final String size; + final String title; + final String duration; + final String thumbnail; + + const DownloadCard( + {super.key, + required this.size, + required this.title, + required this.thumbnail, + required this.duration}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 75, + width: double.maxFinite, + padding: const EdgeInsets.all(15), + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [ + _buildLeadingWrapper(), + const SizedBox(width: 10), + _buildCourseInfo(), + const SizedBox(width: 10), + _buildRemoveButtonWrapper(), + ], + ); + + Widget _buildLeadingWrapper() => SizedBox( + width: 50, + height: double.maxFinite, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: _buildLeadingStack(), + ), + ); + + Widget _buildLeadingStack() => Stack( + alignment: Alignment.center, + children: [_buildImageWrapper(), _buildPlayButtonWrapper()], + ); + + Widget _buildImageWrapper() => + Align(alignment: Alignment.center, child: _buildImage()); + + Widget _buildImage() => Image.asset( + thumbnail, + fit: BoxFit.fill, + width: double.maxFinite, + ); + + Widget _buildPlayButtonWrapper() => Align( + alignment: Alignment.center, + child: _buildPlayButton(), + ); + + Widget _buildPlayButton() => CircleAvatar( + radius: 14, + backgroundColor: kcTransparent, + child: _buildPlayIconClipper(), + ); + + Widget _buildPlayIconClipper() => ClipRRect( + borderRadius: BorderRadius.circular(50), + child: _buildPlayIconBlender(), + ); + + Widget _buildPlayIconBlender() => BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: _buildPlayIcon(), + ); + + Widget _buildPlayIcon() => const Icon( + Icons.play_arrow, + color: kcWhite, + ); + + Widget _buildCourseInfo() => Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildCourseInfoChildren(), + ), + ); + + List _buildCourseInfoChildren() => [_buildTitle(), _buildMiddleRow()]; + + Widget _buildTitle() => Text( + title, + style: style16DG600, + ); + + Widget _buildMiddleRow() => Row( + children: [ + _buildSize(), + const SizedBox(width: 10), + _buildDot(), + const SizedBox(width: 10), + _buildDuration() + ], + ); + + Widget _buildDuration() => Text( + duration, + style: style14P400, + ); + + Widget _buildDot() => Text( + '-', + style: style14P400, + ); + + Widget _buildSize() => Text( + size, + style: style14P400, + ); + + Widget _buildRemoveButtonWrapper() => Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: _buildRemoveButton(), + ); + + Widget _buildRemoveButton() => CustomElevatedButton( + width: 110, + height: 15, + text: 'Remove', + borderRadius: 12, + foregroundColor: kcRed, + backgroundColor: kcRed.withOpacity(0.25), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart index ba30dcc..53a4bac 100644 --- a/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart +++ b/StudioProjects/yimaru_app/lib/ui/widgets/language_button.dart @@ -20,12 +20,12 @@ class LanguageButton extends StatelessWidget { decoration: BoxDecoration( color: kcPrimaryColor, borderRadius: BorderRadius.circular(10), - border: Border.all(color: kcWhiteColor)), + border: Border.all(color: kcWhite)), child: _buildLanguage(), ); Widget _buildLanguage() => Text( language, - style: const TextStyle(color: kcWhiteColor), + style: const TextStyle(color: kcWhite), ); } diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart new file mode 100644 index 0000000..16ba72b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/large_app_bar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/language_button.dart'; + +class LargeAppBar extends StatelessWidget { + final bool showBackButton; + final GestureTapCallback? onTap; + final GestureTapCallback? onPop; + final bool showLanguageSelection; + final GestureTapCallback? onLanguage; + + const LargeAppBar( + {super.key, + this.onTap, + this.onPop, + this.onLanguage, + required this.showBackButton, + required this.showLanguageSelection}); + + @override + Widget build(BuildContext context) => _buildAppBarWrapper(); + + Widget _buildAppBarWrapper() => Container( + height: 125, + width: double.maxFinite, + alignment: Alignment.bottomCenter, + decoration: const BoxDecoration( + color: kcPrimaryColor, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), + ), + ), + padding: const EdgeInsets.only(bottom: 25, right: 15), + child: _buildAppBarItems(), + ); + + Widget _buildAppBarItems() => Stack( + children: _buildAppBarItemChildren(), + ); + + List _buildAppBarItemChildren() => + [if (showBackButton) _buildBackButtonWrapper(), _buildRightButton()]; + + Widget _buildBackButtonWrapper() => Align( + alignment: Alignment.bottomLeft, + child: _buildBackButton(), + ); + + Widget _buildBackButton() => BackButton( + onPressed: onTap, + style: + const ButtonStyle(foregroundColor: WidgetStatePropertyAll(kcWhite)), + ); + + Widget _buildRightButton() => Align( + alignment: Alignment.bottomRight, + child: showLanguageSelection + ? _buildLanguageSelector() + : _buildCloseButton()); + + Widget _buildLanguageSelector() => LanguageButton( + language: 'EN', + onTap: onLanguage, + ); + + Widget _buildCloseButton() => IconButton( + onPressed: () {}, + icon: _buildCloseIcon(), + ); + + Widget _buildCloseIcon() => const Icon( + Icons.close, + color: kcWhite, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart new file mode 100644 index 0000000..e3e331c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_app_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class LearnAppBar extends StatelessWidget { + const LearnAppBar({super.key}); + + @override + Widget build(BuildContext context) => _buildStack(); + + Widget _buildStack() => Stack( + alignment: Alignment.center, + children: _buildStackChildren(), + ); + + List _buildStackChildren() => + [_buildProfileWrapper(), _buildNotificationIconWrapper()]; + + Widget _buildProfileWrapper() => Align( + alignment: Alignment.centerLeft, + child: _buildProfileRow(), + ); + + Widget _buildProfileRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildProfileRowChildren(), + ); + + List _buildProfileRowChildren() => + [_buildProfileImage(), horizontalSpaceSmall, _buildGreetingTextColumn()]; + + Widget _buildProfileImage() => const CircleAvatar( + radius: 25, + backgroundImage: AssetImage('assets/images/profile.png'), + ); + + Widget _buildGreetingTextColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildGreetingChildren(), + ); + + List _buildGreetingChildren() => + [_buildGreetingTitle(), _buildSubTitle()]; + + Widget _buildGreetingTitle() => const Text.rich( + TextSpan( + text: 'Hello,', + style: TextStyle( + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: ' Bisrat!', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ) + ]), + ); + + Widget _buildSubTitle() => const Text( + 'Ready to keep learning English today?', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildNotificationIconWrapper() => + Align(alignment: Alignment.bottomRight, child: _buildNotificationIcon()); + + Widget _buildNotificationIcon() => const Icon( + Icons.notifications_none, + color: kcDarkGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart new file mode 100644 index 0000000..d858595 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_level_tile.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/common/enmus.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/views/learn/learn_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import 'custom_elevated_button.dart'; + +class LearnLevelTile extends ViewModelWidget { + final String title; + final String subtitle; + final LearnLevelStatus status; + + const LearnLevelTile({ + super.key, + required this.title, + required this.status, + required this.subtitle, + }); + + @override + Widget build(BuildContext context, LearnViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: status == LearnLevelStatus.started + ? kcPrimaryColor.withOpacity(0.2) + : kcVeryLightGrey), + ), + child: _buildExpansionTile(viewModel), + ); + + Widget _buildExpansionTile(LearnViewModel viewModel) => ExpansionTile( + textColor: kcDarkGrey, + title: _buildTitleRow(), + subtitle: _buildContent(), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: status != LearnLevelStatus.pending + ? kcPrimaryColor.withOpacity(0.1) + : kcBackgroundColor, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + enabled: status != LearnLevelStatus.pending ? true : false, + collapsedBackgroundColor: status != LearnLevelStatus.pending + ? kcPrimaryColor.withOpacity(0.1) + : kcBackgroundColor, + showTrailingIcon: status != LearnLevelStatus.pending ? true : false, + initiallyExpanded: status == LearnLevelStatus.started ? true : false, + children: _buildExpansionTileChildren(viewModel), + ); + + List _buildExpansionTileChildren(LearnViewModel viewModel) => + [_buildActionButton(viewModel)]; + + Widget _buildTitleRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTitleChildren(), + ); + + List _buildTitleChildren() => [ + _buildTitle(), + if (status != LearnLevelStatus.pending) horizontalSpaceSmall, + if (status != LearnLevelStatus.pending) _buildProgressStatus() + ]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressStatus() => ProgressStatus( + status: status.name.substring(0, 1).toUpperCase() + + status.name.substring(1, status.name.length), + color: kcPrimaryColor, + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildActionButton(LearnViewModel viewModel) => CustomElevatedButton( + height: 15, + borderRadius: 12, + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + text: status == LearnLevelStatus.completed + ? 'Review Course' + : status == LearnLevelStatus.pending + ? 'Start Learning' + : 'Continue Learning', + onTap: () async => await viewModel.navigateToLearnLevel(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart new file mode 100644 index 0000000..679aec9 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_module_tile.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/learn_module/learn_module_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +import '../common/app_colors.dart'; +import '../common/enmus.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class LearnModuleTile extends ViewModelWidget { + final String title; + final String subtitle; + final LearnLevelStatus status; + + const LearnModuleTile({ + super.key, + required this.title, + required this.status, + required this.subtitle, + }); + + IconData _getIcon() { + if (title.contains('Module 1')) { + return Iconsax.cake; + } else if (title.contains('Module 2')) { + return Icons.all_inbox; + } else if (title.contains('Module 3')) { + return Icons.lightbulb_outline; + } else if (title.contains('Module 4')) { + return Icons.search; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context, LearnModuleViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnModuleViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcVeryLightGrey), + ), + child: _buildTileStack(viewModel), + ); + + Widget _buildTileStack(LearnModuleViewModel viewModel) => Stack( + children: [ + _buildExpansionTile(viewModel), + _buildContainerShaderWrapper() + ], + ); + + Widget _buildExpansionTile(LearnModuleViewModel viewModel) => ExpansionTile( + textColor: kcDarkGrey, + title: _buildTitle(), + subtitle: _buildContent(), + leading: _buildIconWrapper(), + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + backgroundColor: kcBackgroundColor, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + collapsedBackgroundColor: kcBackgroundColor, + enabled: status != LearnLevelStatus.pending, + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + childrenPadding: const EdgeInsets.fromLTRB(70, 15, 15, 15), + showTrailingIcon: status != LearnLevelStatus.pending ? true : false, + initiallyExpanded: status == LearnLevelStatus.started ? true : false, + children: _buildExpansionTileChildren(viewModel), + ); + + Widget _buildIconWrapper() => CircleAvatar( + backgroundColor: kcPrimaryColor.withOpacity(0.1), + child: _buildIcon(), + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + List _buildExpansionTileChildren(LearnModuleViewModel viewModel) => + [_buildExpansionTileItem(viewModel)]; + + Widget _buildExpansionTileItem(LearnModuleViewModel viewModel) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildExpansionTileItemChildren(viewModel), + ); + + List _buildExpansionTileItemChildren( + LearnModuleViewModel viewModel) => + [ + _buildProgressRow(), + verticalSpaceSmall, + _buildActionButtonWrapper(viewModel) + ]; + + Widget _buildProgressRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildProgressChildren(), + ); + + List _buildProgressChildren() => + [_buildProgressStatusWrapper(), horizontalSpaceSmall, _buildProgress()]; + + Widget _buildProgressStatusWrapper() => Expanded( + child: _buildProgressStatus(), + ); + + Widget _buildProgressStatus() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey); + + Widget _buildProgress() => const Text( + '2/3', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildActionButtonWrapper(LearnModuleViewModel viewModel) => SizedBox( + height: 40, + child: _buildActionButtons(viewModel), + ); + + Widget _buildActionButtons(LearnModuleViewModel viewModel) => Row( + children: [ + _buildLessonButtonWrapper(viewModel), + horizontalSpaceSmall, + _buildPracticeButtonWrapper(viewModel) + ], + ); + + Widget _buildLessonButtonWrapper(LearnModuleViewModel viewModel) => Expanded( + child: _buildLessonButton(viewModel), + ); + + Widget _buildLessonButton(LearnModuleViewModel viewModel) => + const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Lessons', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + //onTap: () async => await viewModel.navigateToLearnModule(), + ); + + Widget _buildPracticeButtonWrapper(LearnModuleViewModel viewModel) => + Expanded( + child: _buildPracticeButton(viewModel), + ); + + Widget _buildPracticeButton(LearnModuleViewModel viewModel) => + const CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Practices', + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + // onTap: () async => await viewModel.navigateToLearnLevel(), + ); + + Widget _buildContainerShaderWrapper() => Positioned.fill( + child: _buildContainerShader(), + ); + + Widget _buildContainerShader() => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: kcWhite.withOpacity(0.75)), + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart new file mode 100644 index 0000000..b50e48a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learn_sub_level_tile.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:stacked/stacked.dart'; +import 'package:yimaru_app/ui/views/learn_level/learn_level_viewmodel.dart'; +import 'package:yimaru_app/ui/widgets/progress_status.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; + +class LearnSubLevelTile extends ViewModelWidget { + final bool current; + final String title; + final String subtitle; + + const LearnSubLevelTile({ + super.key, + required this.title, + required this.current, + required this.subtitle, + }); + + @override + Widget build(BuildContext context, LearnLevelViewModel viewModel) => + _buildExpansionTileCard(viewModel); + + Widget _buildExpansionTileCard(LearnLevelViewModel viewModel) => Container( + margin: const EdgeInsets.only(bottom: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: + current ? kcPrimaryColor.withOpacity(0.2) : kcVeryLightGrey), + ), + child: _buildExpansionTile(viewModel), + ); + + Widget _buildExpansionTile(LearnLevelViewModel viewModel) => ExpansionTile( + enabled: current, + textColor: kcDarkGrey, + title: _buildTitleRow(), + showTrailingIcon: current, + subtitle: _buildContent(), + initiallyExpanded: current, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + shape: Border.all(color: kcTransparent), + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + backgroundColor: + current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + collapsedBackgroundColor: + current ? kcPrimaryColor.withOpacity(0.1) : kcBackgroundColor, + children: _buildExpansionTileChildren(viewModel), + ); + + List _buildExpansionTileChildren(LearnLevelViewModel viewModel) => + [_buildActionButtonWrapper(viewModel)]; + + Widget _buildTitleRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildTitleChildren(), + ); + + List _buildTitleChildren() => [ + _buildTitle(), + if (current) horizontalSpaceSmall, + if (current) _buildProgressStatus() + ]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressStatus() => const ProgressStatus( + color: kcPrimaryColor, + status: 'Current Level', + ); + + Widget _buildContent() => Text( + subtitle, + style: const TextStyle( + color: kcDarkGrey, + ), + ); + + Widget _buildActionButtonWrapper(LearnLevelViewModel viewModel) => SizedBox( + height: 40, + child: _buildActionButtons(viewModel), + ); + + Widget _buildActionButtons(LearnLevelViewModel viewModel) => Row( + children: [ + _buildViewButtonWrapper(viewModel), + horizontalSpaceSmall, + _buildPracticeButtonWrapper(viewModel) + ], + ); + + Widget _buildViewButtonWrapper(LearnLevelViewModel viewModel) => Expanded( + child: _buildViewButton(viewModel), + ); + + Widget _buildViewButton(LearnLevelViewModel viewModel) => + CustomElevatedButton( + height: 15, + borderRadius: 12, + text: 'View Course', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + onTap: () async => await viewModel.navigateToLearnModule(), + ); + + Widget _buildPracticeButtonWrapper(LearnLevelViewModel viewModel) => Expanded( + child: _buildPracticeButton(viewModel), + ); + + Widget _buildPracticeButton(LearnLevelViewModel viewModel) => + const CustomElevatedButton( + height: 15, + text: 'Practice', + borderRadius: 12, + backgroundColor: kcWhite, + borderColor: kcPrimaryColor, + foregroundColor: kcPrimaryColor, + + // onTap: () async => await viewModel.navigateToLearnLevel(), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart new file mode 100644 index 0000000..f1a5330 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/learning_progress_card.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/widgets/custom_column.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; +import 'custom_elevated_button.dart'; +import 'custom_linear_progress_indicator.dart'; + +class LearningProgressCard extends StatelessWidget { + final GestureTapCallback? onTap; + + const LearningProgressCard({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + height: 320, + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceTiny, + _buildSubtitle(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceSmall, + _buildLearningStatus(), + verticalSpaceMedium, + _buildActionButton(), + ]; + + Widget _buildIcon() => const Icon( + Icons.menu_book_rounded, + size: 50, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => const Text( + 'Learn English', + style: TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubtitle() => const Text( + 'Great job! Keep the momentum.', + maxLines: 2, + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.5, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildLearningStatus() => Row( + children: _buildLearningStatusChildren(), + ); + + List _buildLearningStatusChildren() => [ + _buildWatchedVideos(), + horizontalSpaceSmall, + _buildCompletedPractices(), + horizontalSpaceSmall, + _buildTakenQuizzes() + ]; + + Widget _buildWatchedVideos() => const Expanded( + child: CustomColumn(title: '120', subtitle: 'Videos Watched')); + Widget _buildCompletedPractices() => const Expanded( + child: CustomColumn(title: '85', subtitle: 'Practices Completed')); + Widget _buildTakenQuizzes() => const Expanded( + child: CustomColumn(title: '45', subtitle: 'Quizzes Taken')); + + Widget _buildActionButton() => const CustomElevatedButton( + height: 15, + width: 200, + borderRadius: 12, + text: 'Continue Learning', + foregroundColor: kcWhite, + backgroundColor: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart b/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart new file mode 100644 index 0000000..4c17a03 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/login_account.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class LoginAccount extends StatelessWidget { + final GestureTapCallback? onTap; + const LoginAccount({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildRow(); + + Widget _buildRow() => Row( + children: [ + _buildLeadingText(), + horizontalSpaceTiny, + _buildRegisterTextButton() + ], + ); + + Widget _buildLeadingText() => const Text( + 'Already have an account? ', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildRegisterTextButton() => TextButton( + onPressed: onTap, + style: const ButtonStyle( + alignment: Alignment.centerLeft, + padding: WidgetStatePropertyAll(EdgeInsets.zero)), + child: _buildRegisterText(), + ); + + Widget _buildRegisterText() => const Text( + 'Login', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart b/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart new file mode 100644 index 0000000..f66d66a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/obscure_password.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ObscurePassword extends StatelessWidget { + final bool focus; + final bool obscure; + final GestureTapCallback? onTap; + const ObscurePassword( + {super.key, this.onTap, required this.focus, required this.obscure}); + + @override + Widget build(BuildContext context) => _buildButton(); + + Widget _buildButton() => GestureDetector( + onTap: onTap, + child: _buildIconWrapper(), + ); + + Widget _buildIconWrapper() => + obscure ? _buildObscuredIcon() : _buildUnObscuredIcon(); + + Widget _buildObscuredIcon() => Icon( + Icons.visibility, + color: focus ? kcPrimaryColor : kcLightGrey, + ); + + Widget _buildUnObscuredIcon() => Icon( + Icons.visibility_off, + color: focus ? kcPrimaryColor : kcLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart deleted file mode 100644 index 5344294..0000000 --- a/StudioProjects/yimaru_app/lib/ui/widgets/onboarding_app_bar.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stacked/stacked.dart'; -import 'package:yimaru_app/ui/common/app_colors.dart'; -import 'package:yimaru_app/ui/views/onboarding/onboarding_viewmodel.dart'; -import 'package:yimaru_app/ui/widgets/language_button.dart'; - -class OnboardingAppBar extends ViewModelWidget { - final bool language; - final bool showBackButton; - final bool showLanguageSelection; - - const OnboardingAppBar( - {super.key, - this.language = false, - this.showBackButton = true, - this.showLanguageSelection = true}); - - @override - Widget build(BuildContext context, OnboardingViewModel viewModel) => - _buildAppBarWrapper(viewModel); - - Widget _buildAppBarWrapper(OnboardingViewModel viewModel) => Container( - height: 125, - width: double.maxFinite, - alignment: Alignment.bottomCenter, - decoration: const BoxDecoration( - color: kcPrimaryColor, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24), - ), - ), - padding: const EdgeInsets.only(bottom: 25, right: 15), - child: _buildAppBarItems(viewModel), - ); - - Widget _buildAppBarItems(OnboardingViewModel viewModel) => Stack( - children: _buildAppBarItemChildren(viewModel), - ); - - List _buildAppBarItemChildren(OnboardingViewModel viewModel) => [ - if (showBackButton) _buildBackButtonWrapper(viewModel), - _buildRightButton(viewModel) - ]; - - Widget _buildBackButtonWrapper(OnboardingViewModel viewModel) => Align( - alignment: Alignment.bottomLeft, - child: _buildBackButton(viewModel), - ); - - Widget _buildBackButton(OnboardingViewModel viewModel) => BackButton( - onPressed: ()=> viewModel.pop(language: language), - style: const ButtonStyle( - foregroundColor: WidgetStatePropertyAll(kcWhiteColor)), - ); - - Widget _buildRightButton(OnboardingViewModel viewModel) => Align( - alignment: Alignment.bottomRight, - child: showLanguageSelection - ? _buildLanguageSelector(viewModel) - : _buildCloseButton()); - - Widget _buildLanguageSelector(OnboardingViewModel viewModel) => - LanguageButton( - language: 'EN', - onTap: () => viewModel.next(page: 23), - ); - - Widget _buildCloseButton() => IconButton( - onPressed: () {}, - icon: _buildCloseIcon(), - ); - - Widget _buildCloseIcon() => const Icon( - Icons.close, - color: kcWhiteColor, - ); -} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart b/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart new file mode 100644 index 0000000..de62133 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/option_text_divider.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class OptionTextDivider extends StatelessWidget { + const OptionTextDivider({super.key}); + + @override + Widget build(BuildContext context) => _buildOrTextWrapper(); + + Widget _buildOrTextWrapper() => Row( + children: [ + _buildDividerWrapper(), + horizontalSpaceSmall, + _buildOrText(), + horizontalSpaceSmall, + _buildDividerWrapper() + ], + ); + Widget _buildDividerWrapper() => Expanded(child: _buildDivider()); + + Widget _buildDivider() => const Divider(color: kcVeryLightGrey); + + Widget _buildOrText() => const Text( + 'or', + textAlign: TextAlign.center, + style: TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart b/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart new file mode 100644 index 0000000..1a047b2 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/overall_learn_progress.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; +import 'package:yimaru_app/ui/widgets/custom_linear_progress_indicator.dart'; + +class OverallLearnProgress extends StatelessWidget { + const OverallLearnProgress({super.key}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 25), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildProgressSection(), + ); + + Widget _buildProgressSection() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildProgressSectionChildren(), + ); + + List _buildProgressSectionChildren() => [ + _buildProgressInfoWrapper(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceSmall, + _buildSubtitle() + ]; + + Widget _buildProgressInfoWrapper() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildProgressInfoChildren(), + ); + + List _buildProgressInfoChildren() => + [_buildProgressInfo(), _buildProgress()]; + + Widget _buildProgressInfo() => const Text( + 'Overall Progress', + style: TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgress() => const Text( + '35%', + style: TextStyle( + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressIndicator() => const CustomLinearProgressIndicator( + progress: 0.75, + activeColor: kcPrimaryColor, + backgroundColor: kcVeryLightGrey, + ); + + Widget _buildSubtitle() => const Text( + 'Keep up the great work! You\'re doing amazing.', + style: TextStyle(color: kcDarkGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart b/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart new file mode 100644 index 0000000..e582d57 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/page_loading_indicator.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import 'custom_circular_progress_indicator.dart'; + +class PageLoadingIndicator extends StatelessWidget { + const PageLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) => _buildBody(context); + + Widget _buildBody(BuildContext context) => Material( + color: kcTransparent, + child: _buildContainer(context), + ); + + Widget _buildContainer(BuildContext context) => Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + color: kcBlack.withOpacity(0.3), + child: _buildBoxContainerWrapper(), + ); + + Widget _buildBoxContainerWrapper() => Center( + child: _buildBoxContainer(), + ); + + Widget _buildBoxContainer() => Container( + width: 150, + height: 100, + alignment: Alignment.center, + decoration: BoxDecoration( + color: kcBackgroundColor, + borderRadius: BorderRadius.circular(7), + ), + child: _buildColumnWrapper(), + ); + + Widget _buildColumnWrapper() => Center( + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => + [_buildShimmer(), verticalSpaceSmall, _buildText()]; + + Widget _buildShimmer() => const Center( + child: CustomCircularProgressIndicator(color: kcPrimaryColor), + ); + + Widget _buildText() => const Text( + 'Please wait', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart b/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart new file mode 100644 index 0000000..ef73e55 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/phone_number_prefix.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class PhoneNumberPrefix extends StatelessWidget { + final bool selected; + const PhoneNumberPrefix({ + super.key, + required this.selected, + }); + + @override + Widget build(BuildContext context) => _buildButtonWrapper(); + + Widget _buildButtonWrapper() => Container( + height: 57, + padding: const EdgeInsets.symmetric(horizontal: 15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: selected ? kcPrimaryColor.withOpacity(0.1) : kcWhite, + border: Border.all( + color: selected ? kcPrimaryColor : kcPrimaryColor.withOpacity(0.75), + ), + ), + child: _buildContainerWrapper(), + ); + + Widget _buildContainerWrapper() => Row( + children: _buildButtonRowChildren(), + ); + + List _buildButtonRowChildren() => + [_buildIcon(), horizontalSpaceSmall, _buildText()]; + + Widget _buildText() => const Text( + '+251', + style: TextStyle(color: kcDarkGrey), + ); + + Widget _buildIcon() => Image.asset( + 'assets/icons/flag.png', + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart new file mode 100644 index 0000000..f6529bc --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/privacy_policy_tile.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; + +import '../common/app_colors.dart'; + +class PrivacyPolicyTile extends StatelessWidget { + final String title; + + const PrivacyPolicyTile({super.key, required this.title}); + + IconData _getIcon() { + if (title == 'Introduction') { + return Icons.list_alt; + } else if (title == 'Information We Collect') { + return Icons.all_inbox; + } else if (title == 'How We Use Your Information') { + return Icons.lightbulb_outline; + } else if (title == 'Data Sharing and Disclosure') { + return Icons.share; + } else if (title == 'Your Rights and Choices') { + return Icons.confirmation_num; + } else if (title == 'Data Security') { + return Icons.shield_moon_outlined; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context) => _buildExpansionTileCard(); + + Widget _buildExpansionTileCard() => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildExpansionTile(), + ); + + Widget _buildExpansionTile() => ExpansionTile( + title: _buildTitle(), + iconColor: kcDarkGrey, + textColor: kcDarkGrey, + leading: _buildIcon(), + showTrailingIcon: true, + initiallyExpanded: false, + collapsedIconColor: kcDarkGrey, + collapsedTextColor: kcDarkGrey, + expandedAlignment: Alignment.centerLeft, + childrenPadding: const EdgeInsets.all(15), + backgroundColor: kcPrimaryColor.withOpacity(0.1), + controlAffinity: ListTileControlAffinity.trailing, + expandedCrossAxisAlignment: CrossAxisAlignment.start, + tilePadding: const EdgeInsets.symmetric(horizontal: 15), + collapsedBackgroundColor: kcPrimaryColor.withOpacity(0.1), + shape: Border.all(color: kcPrimaryColor.withOpacity(0.2)), + children: _buildExpansionTileChildren(), + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: kcPrimaryColor, + ); + + List _buildExpansionTileChildren() => [_buildContent()]; + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildContent() => const Text( + ksPrivacyPolicy, + style: TextStyle( + color: kcDarkGrey, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart new file mode 100644 index 0000000..6563a8a --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/profile_card.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ProfileCard extends StatelessWidget { + final String title; + final IconData icon; + final String subTitle; + final GestureTapCallback? onTap; + + const ProfileCard( + {super.key, + this.onTap, + required this.icon, + required this.title, + required this.subTitle}); + + @override + Widget build(BuildContext context) => _buildContainerWrapper(); + + Widget _buildContainerWrapper() => GestureDetector( + onTap: onTap, + child: _buildContainer(), + ); + + Widget _buildContainer() => Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: kcPrimaryColor.withOpacity(0.1), + ), + child: _buildColumn(), + ); + + Widget _buildColumn() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildColumnChildren(), + ); + + List _buildColumnChildren() => [ + _buildIcon(), + verticalSpaceSmall, + _buildTitle(), + verticalSpaceSmall, + _buildSubTitle() + ]; + + Widget _buildIcon() => Icon( + icon, + size: 35, + color: kcPrimaryColor, + ); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildSubTitle() => Text( + subTitle, + style: const TextStyle(color: kcMediumGrey), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart b/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart new file mode 100644 index 0000000..42f61f3 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/profile_image.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ProfileImage extends StatelessWidget { + const ProfileImage({super.key}); + + @override + Widget build(BuildContext context) => _buildSizedBox(); + + Widget _buildSizedBox() => SizedBox( + height: 125, + width: 125, + child: _buildStack(), + ); + + Widget _buildStack() => Stack( + children: [_buildProfileImageWrapper(), _buildCameraButtonWrapper()], + ); + + Widget _buildProfileImageWrapper() => Align( + alignment: Alignment.center, + child: _buildProfileImage(), + ); + + Widget _buildProfileImage() => const CircleAvatar( + radius: 50, + backgroundImage: AssetImage('assets/images/profile.png'), + ); + + Widget _buildCameraButtonWrapper() => Align( + alignment: Alignment.bottomCenter, + child: _buildCameraButton(), + ); + + Widget _buildCameraButton() => Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7), + decoration: BoxDecoration( + color: kcPrimaryColor, borderRadius: BorderRadius.circular(25)), + child: _buildCameraIcon(), + ); + + Widget _buildCameraIcon() => const Icon( + Icons.camera_alt_outlined, + size: 18, + color: kcWhite, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart b/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart new file mode 100644 index 0000000..76ef5a8 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/progress_status.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ProgressStatus extends StatelessWidget { + final Color color; + final String status; + + const ProgressStatus({super.key, required this.color, required this.status}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2.5), + child: _buildLabel(), + ); + + Widget _buildLabel() => Text( + status, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart b/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart new file mode 100644 index 0000000..933712b --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/register_for_account.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import '../common/app_colors.dart'; +import '../common/ui_helpers.dart'; + +class RegisterForAccount extends StatelessWidget { + final GestureTapCallback? onTap; + const RegisterForAccount({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildRow(); + + Widget _buildRow() => Row( + children: [ + _buildLeadingText(), + horizontalSpaceTiny, + _buildRegisterTextButton() + ], + ); + + Widget _buildLeadingText() => const Text( + 'Don’t have an account? ', + style: TextStyle(color: kcMediumGrey), + ); + + Widget _buildRegisterTextButton() => TextButton( + onPressed: onTap, + style: const ButtonStyle( + alignment: Alignment.centerLeft, + padding: WidgetStatePropertyAll(EdgeInsets.zero)), + child: _buildRegisterText(), + ); + + Widget _buildRegisterText() => const Text( + 'Register', + style: TextStyle(color: kcPrimaryColor), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart b/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart new file mode 100644 index 0000000..3543d51 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/skill_progress.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:iconsax/iconsax.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import 'custom_linear_progress_indicator.dart'; + +class SkillProgress extends StatelessWidget { + final String skill; + final double progress; + + const SkillProgress({super.key, required this.skill, required this.progress}); + + Color _getColor() { + if (skill == 'Speaking') { + return kcOrange; + } else if (skill == 'Listening') { + return kcGreen; + } else if (skill == 'Reading') { + return kcAquamarine; + } else { + return kcIndigo; + } + } + + IconData _getIcon() { + if (skill == 'Speaking') { + return Icons.mic_none; + } else if (skill == 'Listening') { + return Icons.headphones_outlined; + } else if (skill == 'Reading') { + return Icons.chrome_reader_mode_outlined; + } else { + return Iconsax.pen_add; + } + } + + @override + Widget build(BuildContext context) => _buildSkillSection(); + + Widget _buildSkillSection() => Column( + mainAxisSize: MainAxisSize.min, + children: _buildStorageSectionChildren(), + ); + + List _buildStorageSectionChildren() => [ + verticalSpaceSmall, + _buildSkillInfoWrapper(), + verticalSpaceSmall, + _buildProgressIndicator(), + verticalSpaceMedium + ]; + + Widget _buildSkillInfoWrapper() => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _buildSkillInfoChildren(), + ); + + List _buildSkillInfoChildren() => + [_buildSkillRow(), _buildProgress()]; + + Widget _buildSkillRow() => Row( + children: [ + _buildIcon(), + const SizedBox( + width: 5, + ), + _buildSkill() + ], + ); + + Widget _buildIcon() => Icon( + _getIcon(), + color: _getColor(), + ); + + Widget _buildSkill() => Text( + skill, + style: const TextStyle( + fontSize: 18, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgress() => Text( + '${(progress * 100).toInt()}%', + style: const TextStyle( + fontSize: 16, + color: kcDarkGrey, + fontWeight: FontWeight.w600, + ), + ); + + Widget _buildProgressIndicator() => CustomLinearProgressIndicator( + progress: progress, + activeColor: _getColor(), + backgroundColor: kcVeryLightGrey, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart b/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart new file mode 100644 index 0000000..97a28af --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/small_app_bar.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/custom_back_button.dart'; + +class SmallAppBar extends StatelessWidget { + final String? title; + final GestureTapCallback? onTap; + + const SmallAppBar({super.key, this.onTap, this.title}); + + @override + Widget build(BuildContext context) => _buildAppBar(); + + Widget _buildAppBar() => Stack( + alignment: Alignment.center, + children: _buildAppBarChildren(), + ); + + List _buildAppBarChildren() => + [_buildBackButtonWrapper(), if (title != null) _buildTitleWrapper()]; + + Widget _buildBackButtonWrapper() => Align( + alignment: Alignment.centerLeft, + child: _buildBackButton(), + ); + + Widget _buildBackButton() => CustomBackButton(onTap: onTap); + + Widget _buildTitleWrapper() => Align( + alignment: Alignment.center, + child: _buildTitle(), + ); + + Widget _buildTitle() => Text( + title ?? '', + style: const TextStyle( + fontSize: 18, + color: kcPrimaryColor, + fontWeight: FontWeight.w600, + ), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart new file mode 100644 index 0000000..88fd382 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/suggestion_card.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_strings.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +import '../common/app_colors.dart'; + +class SuggestionCard extends StatelessWidget { + const SuggestionCard({super.key}); + + @override + Widget build(BuildContext context) => _buildContainer(); + + Widget _buildContainer() => Container( + height: 75, + width: double.maxFinite, + margin: const EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [kcPrimaryAccent, kcPrimaryColor]), + ), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [_buildIcon(), horizontalSpaceSmall, _buildTitleWrapper()], + ); + + Widget _buildIcon() => const Icon(Icons.lightbulb_outline, color: kcWhite); + + Widget _buildTitleWrapper() => Expanded( + child: _buildTitle(), + ); + + Widget _buildTitle() => const Text( + ksSuggestion, + style: TextStyle(color: kcWhite), + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart b/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart new file mode 100644 index 0000000..dd3a92c --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/support_card.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/widgets/circular_icon.dart'; + +class SupportCard extends StatelessWidget { + final Color color; + final String title; + final IconData icon; + final String subtitle; + final GestureTapCallback? onTap; + + const SupportCard({ + super.key, + this.onTap, + required this.icon, + required this.color, + required this.title, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) => _buildLitTile(); + + Widget _buildLitTile() => ListTile( + onTap: onTap, + title: _buildTitle(), + subtitle: _buildSubtitle(), + trailing: _buildTrailingIcon(), + leading: _buildLeadingWrapper(), + tileColor: color.withOpacity(0.2), + ); + + Widget _buildLeadingWrapper() => + CircularIcon(icon: icon, size: 20, color: color); + + Widget _buildTitle() => Text( + title, + style: const TextStyle( + fontSize: 16, color: kcDarkGrey, fontWeight: FontWeight.w600), + ); + + Widget _buildSubtitle() => Text( + subtitle, + maxLines: 2, + style: const TextStyle(color: kcDarkGrey), + ); + + Widget _buildTrailingIcon() => Icon( + Icons.keyboard_arrow_right, + color: color, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart b/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart new file mode 100644 index 0000000..b77e981 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/validator_list_tile.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; +import 'package:yimaru_app/ui/common/ui_helpers.dart'; + +class ValidatorListTile extends StatelessWidget { + final String label; + final Color backgroundColor; + const ValidatorListTile( + {super.key, required this.label, required this.backgroundColor}); + + @override + Widget build(BuildContext context) => _buildRowWrapper(); + + Widget _buildRowWrapper() => Padding( + padding: const EdgeInsets.only(bottom: 15), + child: _buildRow(), + ); + + Widget _buildRow() => Row( + children: [_buildIconWrapper(), horizontalSpaceSmall, _buildLabel()], + ); + + Widget _buildIconWrapper() => CircleAvatar( + radius: 8, + backgroundColor: backgroundColor, + child: _buildIcon(), + ); + + Widget _buildIcon() => const Icon( + Icons.check, + size: 14, + color: kcWhite, + ); + + Widget _buildLabel() => Text( + label, + style: style16DG400, + ); +} diff --git a/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart b/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart new file mode 100644 index 0000000..fe45130 --- /dev/null +++ b/StudioProjects/yimaru_app/lib/ui/widgets/view_profile_button.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:yimaru_app/ui/common/app_colors.dart'; + +class ViewProfileButton extends StatelessWidget { + final GestureTapCallback? onTap; + const ViewProfileButton({super.key, this.onTap}); + + @override + Widget build(BuildContext context) => _buildButtonWrapper(); + + Widget _buildButtonWrapper() => GestureDetector( + onTap: onTap, + child: _buildButtonRow(), + ); + + Widget _buildButtonRow() => Row( + mainAxisSize: MainAxisSize.min, + children: _buildButtonRowChildren(), + ); + + List _buildButtonRowChildren() => + [_buildButtonText(), const SizedBox(width: 10), _buildButtonIcon()]; + + Widget _buildButtonText() => const Text( + 'View Profile', + style: TextStyle( + color: kcPrimaryColor, fontSize: 16, fontWeight: FontWeight.w900), + ); + + Widget _buildButtonIcon() => const Icon( + Icons.arrow_forward, + size: 16, + color: kcPrimaryColor, + ); +} diff --git a/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc b/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc +++ b/StudioProjects/yimaru_app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake b/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake +++ b/StudioProjects/yimaru_app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift b/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..61d01d0 100644 --- a/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/StudioProjects/yimaru_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_darwin +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/StudioProjects/yimaru_app/pubspec.lock b/StudioProjects/yimaru_app/pubspec.lock index da6e4ea..9e6e165 100644 --- a/StudioProjects/yimaru_app/pubspec.lock +++ b/StudioProjects/yimaru_app/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: a2cebb899f91d36eeeaa55c7b20b5915db5a9df1b8fd4a3c9c825e22e474537d + url: "https://pub.dev" + source: hosted + version: "9.1.0" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" dart_style: dependency: transitive description: @@ -169,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" dropdown_search: dependency: "direct main" description: @@ -177,6 +209,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + email_validator: + dependency: "direct main" + description: + name: email_validator + sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb + url: "https://pub.dev" + source: hosted + version: "3.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -185,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" file: dependency: transitive description: @@ -206,6 +262,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: transitive + description: + name: flutter_bloc + sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38 + url: "https://pub.dev" + source: hosted + version: "9.1.1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_lints: dependency: "direct dev" description: @@ -214,6 +286,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_svg: dependency: "direct main" description: @@ -227,6 +347,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timer_countdown: + dependency: "direct main" + description: + name: flutter_timer_countdown + sha256: "0c73e1593ad7949c007752199a17e7ed50bb581568743dbc32f061f49873219e" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed_annotation: dependency: transitive description: @@ -283,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -315,6 +456,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.8" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -332,13 +489,21 @@ packages: source: hosted version: "0.7.2" json_annotation: - dependency: transitive + dependency: "direct main" description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted version: "4.9.0" + json_serializable: + dependency: "direct main" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" leak_tracker: dependency: transitive description: @@ -371,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logger: dependency: transitive description: @@ -435,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + omni_datetime_picker: + dependency: "direct main" + description: + name: omni_datetime_picker + sha256: bb360790e76109ea2e53b45643cdaab779649c4bf1a9d2794d3a135bfe9746e1 + url: "https://pub.dev" + source: hosted + version: "2.3.1" package_config: dependency: transitive description: @@ -459,6 +640,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" petitparser: dependency: transitive description: @@ -467,6 +704,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "692e1c29703abefad6c502e97b4d946d506384397438ea242afadbfe48354819" + url: "https://pub.dev" + source: hosted + version: "6.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -507,6 +768,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -536,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" source_span: dependency: transitive description: @@ -632,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" + url: "https://pub.dev" + source: hosted + version: "3.0.3" typed_data: dependency: transitive description: @@ -648,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" vector_graphics: dependency: transitive description: @@ -720,6 +1013,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" xdg_directories: dependency: transitive description: @@ -745,5 +1046,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/StudioProjects/yimaru_app/pubspec.yaml b/StudioProjects/yimaru_app/pubspec.yaml index 16c45fd..0a57982 100644 --- a/StudioProjects/yimaru_app/pubspec.yaml +++ b/StudioProjects/yimaru_app/pubspec.yaml @@ -9,13 +9,24 @@ environment: dependencies: flutter: sdk: flutter + intl: any + dio: ^5.9.0 + pinput: ^6.0.1 stacked: ^3.4.0 iconsax: ^0.0.8 flutter_svg: ^2.2.3 - dropdown_search: ^6.0.2 - stacked_services: ^1.1.0 - stacked_shared: any + flutter_html: ^3.0.0 + email_validator: any + toastification: ^3.0.3 + dropdown_search: ^6.0.2 + json_annotation: ^4.9.0 + stacked_services: ^1.1.0 + omni_datetime_picker: any + json_serializable: ^6.8.0 + flutter_secure_storage: ^10.0.0 + flutter_timer_countdown: ^1.0.7 + dev_dependencies: flutter_test: sdk: flutter @@ -25,6 +36,9 @@ dev_dependencies: golden_toolkit: ^0.15.0 stacked_generator: ^1.3.3 +dependency_overrides: + intl: ^0.20.2 + flutter: uses-material-design: true diff --git a/StudioProjects/yimaru_app/test/helpers/test_helpers.dart b/StudioProjects/yimaru_app/test/helpers/test_helpers.dart index df3b057..ae13e93 100644 --- a/StudioProjects/yimaru_app/test/helpers/test_helpers.dart +++ b/StudioProjects/yimaru_app/test/helpers/test_helpers.dart @@ -2,6 +2,10 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:yimaru_app/app/app.locator.dart'; import 'package:stacked_services/stacked_services.dart'; +import 'package:yimaru_app/services/authentication_service.dart'; +import 'package:yimaru_app/services/api_service.dart'; +import 'package:yimaru_app/services/secure_storage_service.dart'; +import 'package:yimaru_app/services/dio_service.dart'; // @stacked-import import 'test_helpers.mocks.dart'; @@ -12,14 +16,22 @@ import 'test_helpers.mocks.dart'; MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), - // @stacked-mock-spec + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), +// @stacked-mock-spec ], ) void registerServices() { getAndRegisterNavigationService(); getAndRegisterBottomSheetService(); getAndRegisterDialogService(); - // @stacked-mock-register + getAndRegisterAuthenticationService(); + getAndRegisterApiService(); + getAndRegisterSecureStorageService(); + getAndRegisterDioService(); +// @stacked-mock-register } MockNavigationService getAndRegisterNavigationService() { @@ -76,6 +88,33 @@ MockDialogService getAndRegisterDialogService() { return service; } +MockAuthenticationService getAndRegisterAuthenticationService() { + _removeRegistrationIfExists(); + final service = MockAuthenticationService(); + locator.registerSingleton(service); + return service; +} + +MockApiService getAndRegisterApiService() { + _removeRegistrationIfExists(); + final service = MockApiService(); + locator.registerSingleton(service); + return service; +} + +MockSecureStorageService getAndRegisterSecureStorageService() { + _removeRegistrationIfExists(); + final service = MockSecureStorageService(); + locator.registerSingleton(service); + return service; +} + +MockDioService getAndRegisterDioService() { + _removeRegistrationIfExists(); + final service = MockDioService(); + locator.registerSingleton(service); + return service; +} // @stacked-mock-create void _removeRegistrationIfExists() { diff --git a/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart b/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart index 651298f..99a2c8b 100644 --- a/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart +++ b/StudioProjects/yimaru_app/test/helpers/test_helpers.mocks.dart @@ -3,13 +3,18 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; -import 'dart:ui' as _i6; +import 'dart:async' as _i6; +import 'dart:ui' as _i7; -import 'package:flutter/material.dart' as _i4; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i3; -import 'package:stacked_services/stacked_services.dart' as _i2; +import 'package:mockito/src/dummies.dart' as _i4; +import 'package:stacked_services/stacked_services.dart' as _i3; +import 'package:yimaru_app/models/user_model.dart' as _i2; +import 'package:yimaru_app/services/api_service.dart' as _i9; +import 'package:yimaru_app/services/authentication_service.dart' as _i8; +import 'package:yimaru_app/services/dio_service.dart' as _i11; +import 'package:yimaru_app/services/secure_storage_service.dart' as _i10; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -24,18 +29,28 @@ import 'package:stacked_services/stacked_services.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +class _FakeUserModel_0 extends _i1.SmartFake implements _i2.UserModel { + _FakeUserModel_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + /// A class which mocks [NavigationService]. /// /// See the documentation for Mockito's code generation for more information. -class MockNavigationService extends _i1.Mock implements _i2.NavigationService { +class MockNavigationService extends _i1.Mock implements _i3.NavigationService { @override String get previousRoute => (super.noSuchMethod( Invocation.getter(#previousRoute), - returnValue: _i3.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.getter(#previousRoute), ), - returnValueForMissingStub: _i3.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.getter(#previousRoute), ), @@ -44,25 +59,25 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { @override String get currentRoute => (super.noSuchMethod( Invocation.getter(#currentRoute), - returnValue: _i3.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.getter(#currentRoute), ), - returnValueForMissingStub: _i3.dummyValue( + returnValueForMissingStub: _i4.dummyValue( this, Invocation.getter(#currentRoute), ), ) as String); @override - _i4.GlobalKey<_i4.NavigatorState>? nestedNavigationKey(int? index) => + _i5.GlobalKey<_i5.NavigatorState>? nestedNavigationKey(int? index) => (super.noSuchMethod( Invocation.method( #nestedNavigationKey, [index], ), returnValueForMissingStub: null, - ) as _i4.GlobalKey<_i4.NavigatorState>?); + ) as _i5.GlobalKey<_i5.NavigatorState>?); @override void config({ @@ -71,7 +86,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { bool? defaultOpaqueRoute, Duration? defaultDurationTransition, bool? defaultGlobalState, - _i2.Transition? defaultTransitionStyle, + _i3.Transition? defaultTransitionStyle, String? defaultTransition, }) => super.noSuchMethod( @@ -92,18 +107,18 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { ); @override - _i5.Future? navigateWithTransition( - _i4.Widget? page, { + _i6.Future? navigateWithTransition( + _i5.Widget? page, { bool? opaque, String? transition = r'', Duration? duration, bool? popGesture, int? id, - _i4.Curve? curve, + _i5.Curve? curve, bool? fullscreenDialog = false, bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, + _i3.Transition? transitionClass, + _i3.Transition? transitionStyle, String? routeName, }) => (super.noSuchMethod( @@ -125,21 +140,21 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? replaceWithTransition( - _i4.Widget? page, { + _i6.Future? replaceWithTransition( + _i5.Widget? page, { bool? opaque, String? transition = r'', Duration? duration, bool? popGesture, int? id, - _i4.Curve? curve, + _i5.Curve? curve, bool? fullscreenDialog = false, bool? preventDuplicates = true, - _i2.Transition? transitionClass, - _i2.Transition? transitionStyle, + _i3.Transition? transitionClass, + _i3.Transition? transitionStyle, String? routeName, }) => (super.noSuchMethod( @@ -161,7 +176,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override bool back({ @@ -183,7 +198,7 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { @override void popUntil( - _i4.RoutePredicate? predicate, { + _i5.RoutePredicate? predicate, { int? id, }) => super.noSuchMethod( @@ -205,13 +220,13 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { ); @override - _i5.Future? navigateTo( + _i6.Future? navigateTo( String? routeName, { dynamic arguments, int? id, bool? preventDuplicates = true, Map? parameters, - _i4.RouteTransitionsBuilder? transition, + _i5.RouteTransitionsBuilder? transition, }) => (super.noSuchMethod( Invocation.method( @@ -226,21 +241,21 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? navigateToView( - _i4.Widget? view, { + _i6.Future? navigateToView( + _i5.Widget? view, { dynamic arguments, int? id, bool? opaque, - _i4.Curve? curve, + _i5.Curve? curve, Duration? duration, bool? fullscreenDialog = false, bool? popGesture, bool? preventDuplicates = true, - _i2.Transition? transition, - _i2.Transition? transitionStyle, + _i3.Transition? transition, + _i3.Transition? transitionStyle, }) => (super.noSuchMethod( Invocation.method( @@ -260,16 +275,16 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? replaceWith( + _i6.Future? replaceWith( String? routeName, { dynamic arguments, int? id, bool? preventDuplicates = true, Map? parameters, - _i4.RouteTransitionsBuilder? transition, + _i5.RouteTransitionsBuilder? transition, }) => (super.noSuchMethod( Invocation.method( @@ -284,10 +299,10 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearStackAndShow( + _i6.Future? clearStackAndShow( String? routeName, { dynamic arguments, int? id, @@ -304,11 +319,11 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearStackAndShowView( - _i4.Widget? view, { + _i6.Future? clearStackAndShowView( + _i5.Widget? view, { dynamic arguments, int? id, }) => @@ -322,10 +337,10 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearTillFirstAndShow( + _i6.Future? clearTillFirstAndShow( String? routeName, { dynamic arguments, int? id, @@ -344,11 +359,11 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? clearTillFirstAndShowView( - _i4.Widget? view, { + _i6.Future? clearTillFirstAndShowView( + _i5.Widget? view, { dynamic arguments, int? id, }) => @@ -362,12 +377,12 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); @override - _i5.Future? pushNamedAndRemoveUntil( + _i6.Future? pushNamedAndRemoveUntil( String? routeName, { - _i4.RoutePredicate? predicate, + _i5.RoutePredicate? predicate, dynamic arguments, int? id, }) => @@ -382,16 +397,16 @@ class MockNavigationService extends _i1.Mock implements _i2.NavigationService { }, ), returnValueForMissingStub: null, - ) as _i5.Future?); + ) as _i6.Future?); } /// A class which mocks [BottomSheetService]. /// /// See the documentation for Mockito's code generation for more information. class MockBottomSheetService extends _i1.Mock - implements _i2.BottomSheetService { + implements _i3.BottomSheetService { @override - void setCustomSheetBuilders(Map? builders) => + void setCustomSheetBuilders(Map? builders) => super.noSuchMethod( Invocation.method( #setCustomSheetBuilders, @@ -401,7 +416,7 @@ class MockBottomSheetService extends _i1.Mock ); @override - _i5.Future<_i2.SheetResponse?> showBottomSheet({ + _i6.Future<_i3.SheetResponse?> showBottomSheet({ required String? title, String? description, String? confirmButtonTitle = r'Ok', @@ -434,13 +449,13 @@ class MockBottomSheetService extends _i1.Mock #elevation: elevation, }, ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), + returnValue: _i6.Future<_i3.SheetResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); + _i6.Future<_i3.SheetResponse?>.value(), + ) as _i6.Future<_i3.SheetResponse?>); @override - _i5.Future<_i2.SheetResponse?> showCustomSheet({ + _i6.Future<_i3.SheetResponse?> showCustomSheet({ dynamic variant, String? title, String? description, @@ -453,7 +468,7 @@ class MockBottomSheetService extends _i1.Mock bool? showIconInAdditionalButton = false, String? additionalButtonTitle, bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), + _i7.Color? barrierColor = const _i7.Color(2315255808), double? elevation = 1.0, bool? barrierDismissible = true, bool? isScrollControlled = false, @@ -497,12 +512,12 @@ class MockBottomSheetService extends _i1.Mock #useRootNavigator: useRootNavigator, }, ), - returnValue: _i5.Future<_i2.SheetResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.SheetResponse?>.value(), - ) as _i5.Future<_i2.SheetResponse?>); + returnValue: _i6.Future<_i3.SheetResponse?>.value(), + returnValueForMissingStub: _i6.Future<_i3.SheetResponse?>.value(), + ) as _i6.Future<_i3.SheetResponse?>); @override - void completeSheet(_i2.SheetResponse? response) => + void completeSheet(_i3.SheetResponse? response) => super.noSuchMethod( Invocation.method( #completeSheet, @@ -515,10 +530,10 @@ class MockBottomSheetService extends _i1.Mock /// A class which mocks [DialogService]. /// /// See the documentation for Mockito's code generation for more information. -class MockDialogService extends _i1.Mock implements _i2.DialogService { +class MockDialogService extends _i1.Mock implements _i3.DialogService { @override void registerCustomDialogBuilders( - Map? builders) => + Map? builders) => super.noSuchMethod( Invocation.method( #registerCustomDialogBuilders, @@ -530,10 +545,10 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { @override void registerCustomDialogBuilder({ required dynamic variant, - required _i4.Widget Function( - _i4.BuildContext, - _i2.DialogRequest, - dynamic Function(_i2.DialogResponse), + required _i5.Widget Function( + _i5.BuildContext, + _i3.DialogRequest, + dynamic Function(_i3.DialogResponse), )? builder, }) => super.noSuchMethod( @@ -549,17 +564,17 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { ); @override - _i5.Future<_i2.DialogResponse?> showDialog({ + _i6.Future<_i3.DialogResponse?> showDialog({ String? title, String? description, String? cancelTitle, - _i6.Color? cancelTitleColor, + _i7.Color? cancelTitleColor, String? buttonTitle = r'Ok', - _i6.Color? buttonTitleColor, + _i7.Color? buttonTitleColor, bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i2.DialogPlatform? dialogPlatform, + _i5.RouteSettings? routeSettings, + _i5.GlobalKey<_i5.NavigatorState>? navigatorKey, + _i3.DialogPlatform? dialogPlatform, }) => (super.noSuchMethod( Invocation.method( @@ -578,13 +593,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #dialogPlatform: dialogPlatform, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), + returnValue: _i6.Future<_i3.DialogResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - _i5.Future<_i2.DialogResponse?> showCustomDialog({ + _i6.Future<_i3.DialogResponse?> showCustomDialog({ dynamic variant, String? title, String? description, @@ -597,13 +612,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { bool? showIconInAdditionalButton = false, String? additionalButtonTitle, bool? takesInput = false, - _i6.Color? barrierColor = const _i6.Color(2315255808), + _i7.Color? barrierColor = const _i7.Color(2315255808), bool? barrierDismissible = false, String? barrierLabel = r'', bool? useSafeArea = true, - _i4.RouteSettings? routeSettings, - _i4.GlobalKey<_i4.NavigatorState>? navigatorKey, - _i4.RouteTransitionsBuilder? transitionBuilder, + _i5.RouteSettings? routeSettings, + _i5.GlobalKey<_i5.NavigatorState>? navigatorKey, + _i5.RouteTransitionsBuilder? transitionBuilder, dynamic customData, R? data, }) => @@ -635,21 +650,21 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #data: data, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), - returnValueForMissingStub: _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + returnValue: _i6.Future<_i3.DialogResponse?>.value(), + returnValueForMissingStub: _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - _i5.Future<_i2.DialogResponse?> showConfirmationDialog({ + _i6.Future<_i3.DialogResponse?> showConfirmationDialog({ String? title, String? description, String? cancelTitle = r'Cancel', - _i6.Color? cancelTitleColor, + _i7.Color? cancelTitleColor, String? confirmationTitle = r'Ok', - _i6.Color? confirmationTitleColor, + _i7.Color? confirmationTitleColor, bool? barrierDismissible = false, - _i4.RouteSettings? routeSettings, - _i2.DialogPlatform? dialogPlatform, + _i5.RouteSettings? routeSettings, + _i3.DialogPlatform? dialogPlatform, }) => (super.noSuchMethod( Invocation.method( @@ -667,13 +682,13 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { #dialogPlatform: dialogPlatform, }, ), - returnValue: _i5.Future<_i2.DialogResponse?>.value(), + returnValue: _i6.Future<_i3.DialogResponse?>.value(), returnValueForMissingStub: - _i5.Future<_i2.DialogResponse?>.value(), - ) as _i5.Future<_i2.DialogResponse?>); + _i6.Future<_i3.DialogResponse?>.value(), + ) as _i6.Future<_i3.DialogResponse?>); @override - void completeDialog(_i2.DialogResponse? response) => + void completeDialog(_i3.DialogResponse? response) => super.noSuchMethod( Invocation.method( #completeDialog, @@ -682,3 +697,235 @@ class MockDialogService extends _i1.Mock implements _i2.DialogService { returnValueForMissingStub: null, ); } + +/// A class which mocks [AuthenticationService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAuthenticationService extends _i1.Mock + implements _i8.AuthenticationService { + @override + _i6.Future userLoggedIn() => (super.noSuchMethod( + Invocation.method( + #userLoggedIn, + [], + ), + returnValue: _i6.Future.value(false), + returnValueForMissingStub: _i6.Future.value(false), + ) as _i6.Future); + + @override + _i6.Future saveUserData(Map? data) => + (super.noSuchMethod( + Invocation.method( + #saveUserData, + [data], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i2.UserModel> getUser() => (super.noSuchMethod( + Invocation.method( + #getUser, + [], + ), + returnValue: _i6.Future<_i2.UserModel>.value(_FakeUserModel_0( + this, + Invocation.method( + #getUser, + [], + ), + )), + returnValueForMissingStub: + _i6.Future<_i2.UserModel>.value(_FakeUserModel_0( + this, + Invocation.method( + #getUser, + [], + ), + )), + ) as _i6.Future<_i2.UserModel>); + + @override + _i6.Future logOut() => (super.noSuchMethod( + Invocation.method( + #logOut, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [ApiService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockApiService extends _i1.Mock implements _i9.ApiService { + @override + _i6.Future> register(Map? data) => + (super.noSuchMethod( + Invocation.method( + #register, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> login(Map? data) => + (super.noSuchMethod( + Invocation.method( + #login, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> verifyOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #verifyOtp, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> resendOtp(Map? data) => + (super.noSuchMethod( + Invocation.method( + #resendOtp, + [data], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); + + @override + _i6.Future> getProfileStatus(_i2.UserModel? user) => + (super.noSuchMethod( + Invocation.method( + #getProfileStatus, + [user], + ), + returnValue: + _i6.Future>.value({}), + returnValueForMissingStub: + _i6.Future>.value({}), + ) as _i6.Future>); +} + +/// A class which mocks [SecureStorageService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSecureStorageService extends _i1.Mock + implements _i10.SecureStorageService { + @override + _i6.Future clear() => (super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [DioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockDioService extends _i1.Mock implements _i11.DioService {} diff --git a/StudioProjects/yimaru_app/test/services/api_service_test.dart b/StudioProjects/yimaru_app/test/services/api_service_test.dart new file mode 100644 index 0000000..93e9612 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/api_service_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ApiServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/authentication_service_test.dart b/StudioProjects/yimaru_app/test/services/authentication_service_test.dart new file mode 100644 index 0000000..a06da40 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/authentication_service_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('AuthenticationServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/dio_service_test.dart b/StudioProjects/yimaru_app/test/services/dio_service_test.dart new file mode 100644 index 0000000..0651d4c --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/dio_service_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('DioServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart b/StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart new file mode 100644 index 0000000..d6f4985 --- /dev/null +++ b/StudioProjects/yimaru_app/test/services/secure_storage_service_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('SecureStorageServiceTest -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart new file mode 100644 index 0000000..d687122 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/account_privacy_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('AccountPrivacyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart new file mode 100644 index 0000000..52150a2 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/call_support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('CallSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart new file mode 100644 index 0000000..342bdcd --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/downloads_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('DownloadsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart index 5405153..0c5d802 100644 --- a/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart +++ b/StudioProjects/yimaru_app/test/viewmodels/home_viewmodel_test.dart @@ -14,14 +14,6 @@ void main() { setUp(() => registerServices()); tearDown(() => locator.reset()); - group('incrementCounter -', () { - test('When called once should return Counter is: 1', () { - final model = getModel(); - model.incrementCounter(); - expect(model.counterLabel, 'Counter is: 1'); - }); - }); - group('showBottomSheet -', () { test( 'When called, should show custom bottom sheet using notice variant', diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart new file mode 100644 index 0000000..5d97e54 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_level_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnLevelViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart new file mode 100644 index 0000000..0080f51 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_module_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnModuleViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart new file mode 100644 index 0000000..cf11c9c --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/learn_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LearnViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart new file mode 100644 index 0000000..8ff6466 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/login_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('LoginViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart new file mode 100644 index 0000000..94a3ed4 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/ongoing_progress_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('OngoingProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart new file mode 100644 index 0000000..81c404c --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/privacy_policy_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('PrivacyPolicyViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart new file mode 100644 index 0000000..a1a9504 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/profile_detail_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProfileDetailViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart new file mode 100644 index 0000000..c72bc93 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/profile_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProfileViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart new file mode 100644 index 0000000..6f79cf5 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/progress_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('ProgressViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart new file mode 100644 index 0000000..78e03a5 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/register_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('RegisterViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart new file mode 100644 index 0000000..260dc47 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('SupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart new file mode 100644 index 0000000..1605ed4 --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/telegram_support_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('TelegramSupportViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart b/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart new file mode 100644 index 0000000..7da4d4a --- /dev/null +++ b/StudioProjects/yimaru_app/test/viewmodels/terms_and_conditions_viewmodel_test.dart @@ -0,0 +1,11 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yimaru_app/app/app.locator.dart'; + +import '../helpers/test_helpers.dart'; + +void main() { + group('TermsAndConditionsViewModel Tests -', () { + setUp(() => registerServices()); + tearDown(() => locator.reset()); + }); +} diff --git a/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc b/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc index 8b6d468..0c50753 100644 --- a/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc +++ b/StudioProjects/yimaru_app/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake b/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake index b93c4c3..4fc759c 100644 --- a/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake +++ b/StudioProjects/yimaru_app/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST