diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index 5d5be65..4769278 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -138,7 +138,7 @@ class AuthenticationRepository extends GetxController { bool biometricSuccess = await attemptBiometricLogin(); if (!biometricSuccess) { if (isFirstTime) { - _navigateToRoute(AppRoutes.signIn); + _navigateToRoute(AppRoutes.onboarding); } else { _navigateToRoute(AppRoutes.onboarding); } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart index 6a07739..9b3b719 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signin/signin_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get/get.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.dart'; @@ -9,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart'; import 'package:sigap/src/shared/widgets/text/custom_text_field.dart'; +import 'package:sigap/src/utils/helpers/helper_functions.dart'; import 'package:sigap/src/utils/validators/validation.dart'; class SignInScreen extends StatelessWidget { @@ -20,23 +20,13 @@ class SignInScreen extends StatelessWidget { final formKey = GlobalKey(); // Get the controller - use Get.put to ensure it's initialized - final controller = Get.put(SignInController()); - - // Check if dark mode is enabled - final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final controller = Get.find(); - // Set system overlay style based on theme - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: - isDarkMode ? Brightness.light : Brightness.dark, - ), - ); + // Check if dark mode is enabled + final isDarkMode = THelperFunctions.isDarkMode(context); return Scaffold( // Use dynamic background color from theme - backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( child: SingleChildScrollView( child: Padding( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart index 1ae6b93..acb67d7 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart @@ -33,7 +33,7 @@ class FormRegistrationScreen extends StatelessWidget { ); return Scaffold( - backgroundColor: dark ? TColors.darkContainer : TColors.lightContainer, + backgroundColor: dark ? TColors.dark : TColors.lightContainer, appBar: _buildAppBar(context, dark), body: Obx(() { // Show loading state while controller initializes diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart index 95c0359..3a90461 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -25,14 +24,6 @@ class SignupWithRoleScreen extends StatelessWidget { final theme = Theme.of(context); final isDark = THelperFunctions.isDarkMode(context); - // Set system overlay style - SystemChrome.setSystemUIOverlayStyle( - SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, - ), - ); - return Scaffold( body: Obx( () => NestedScrollView( @@ -223,7 +214,7 @@ class SignupWithRoleScreen extends StatelessWidget { return Container( decoration: BoxDecoration( - color: isDark ? TColors.darkContainer : TColors.lightContainer, + color: isDark ? TColors.dark : TColors.lightContainer, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), @@ -245,7 +236,7 @@ class SignupWithRoleScreen extends StatelessWidget { child: Container( height: 60, decoration: BoxDecoration( - color: isDark ? TColors.darkContainer : TColors.lightContainer, + color: isDark ? TColors.dark : TColors.lightContainer, borderRadius: BorderRadius.circular(TSizes.borderRadiusLg), ), child: Row( @@ -511,7 +502,7 @@ class SignupWithRoleScreen extends StatelessWidget { text: 'Google', iconImage: TImages.googleIcon, onPressed: () => controller.signInWithGoogle(), - backgroundColor: isDark ? TColors.darkContainer : Colors.white, + backgroundColor: isDark ? TColors.dark : Colors.white, foregroundColor: isDark ? TColors.white : TColors.dark, borderColor: isDark ? Colors.transparent : Colors.grey.shade300, ), @@ -523,7 +514,7 @@ class SignupWithRoleScreen extends StatelessWidget { text: 'Facebook', iconImage: TImages.facebookIcon, onPressed: () => controller.signInWithFacebook(), - backgroundColor: isDark ? TColors.darkContainer : Colors.white, + backgroundColor: isDark ? TColors.dark : Colors.white, foregroundColor: isDark ? TColors.white : TColors.dark, borderColor: isDark ? Colors.transparent : Colors.grey.shade300, ), diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart index 82d4caf..5c9fff8 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/password_field.dart @@ -82,7 +82,7 @@ class PasswordField extends StatelessWidget { ), filled: true, fillColor: - isDark ? TColors.darkContainer : TColors.lightContainer, + isDark ? TColors.dark : TColors.lightContainer, border: OutlineInputBorder( borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), borderSide: BorderSide(color: TColors.borderPrimary, width: 1), diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart index 468f039..3f1487e 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/onboarding_controller.dart @@ -1,13 +1,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:logger/logger.dart'; import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/features/onboarding/data/dummy/onboarding_dummy.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; -import 'package:sigap/src/utils/constants/image_strings.dart'; -import 'package:sigap/src/utils/popups/full_screen_loader.dart'; -import 'package:sigap/src/utils/popups/loaders.dart'; +import 'package:sigap/src/utils/popups/circular_full_screen_loader.dart'; class OnboardingController extends GetxController with GetSingleTickerProviderStateMixin { @@ -23,6 +20,8 @@ class OnboardingController extends GetxController // Observable variables final RxInt currentIndex = 0.obs; final PageController pageController = PageController(initialPage: 0); + // Tambahkan controller untuk PageView teks + final PageController textPageController = PageController(initialPage: 0); final RxBool isLocationChecking = false.obs; // Animation controllers @@ -57,6 +56,7 @@ class OnboardingController extends GetxController @override void onClose() { pageController.dispose(); + textPageController.dispose(); // dispose controller baru animationController.dispose(); super.onClose(); } @@ -66,6 +66,7 @@ class OnboardingController extends GetxController currentIndex.value = index; animationController.reset(); animationController.forward(); + // Sinkronisasi textPageController dilakukan di widget, tidak perlu di sini } // Method to go to next page @@ -97,53 +98,40 @@ class OnboardingController extends GetxController isLocationChecking.value = true; try { - - TFullScreenLoader.openLoadingDialog( - 'Checking location...', - TImages.loader, - ); + TCircularFullScreenLoader.openLoadingDialog(text: 'Checking location...'); // Verify location is valid (in Jember and not mocked) final isLocationValid = await _locationService.isLocationValidForFeature(); - TFullScreenLoader.stopLoading(); - - Logger().i('isFirstTime before: ${storage.read('isFirstTime')}'); + TCircularFullScreenLoader.stopLoading(); storage.write('isFirstTime', false); - Logger().i('isFirstTime after: ${storage.read('isFirstTime')}'); - if (isLocationValid) { // If location is valid, proceed to role selection - Get.offAllNamed(AppRoutes.signupWithRole); - - // TLoaders.successSnackBar( - // title: 'Location Valid', - // message: 'Checking location was successful', - // ); + Get.offAllNamed(AppRoutes.roleSelection); // Store isfirstTime to false in storage } else { // If location is invalid, show warning screen + // TLoaders.errorSnackBar( + // title: 'Location Invalid', + // message: + // 'Please enable location services or ensure you are in Jember area', + // ); + Get.offAllNamed(AppRoutes.locationWarning); - TLoaders.errorSnackBar( - title: 'Location Invalid', - message: - 'Please enable location services or ensure you are in Jember area', - ); } - } catch (e) { // If there's an error, show the location warning screen // TFullScreenLoader.stopLoading(); Get.offAllNamed(AppRoutes.locationWarning); } finally { isLocationChecking.value = false; - // TFullScreenLoader.stopLoading(); + // TCircularFullScreenLoader.stopLoading(); } } diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart index fc0743a..e390ac0 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart @@ -5,6 +5,7 @@ import 'package:sigap/src/cores/services/location_service.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/constants/image_strings.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; +import 'package:sigap/src/utils/popups/loaders.dart'; class LocationWarningScreen extends StatelessWidget { const LocationWarningScreen({super.key}); @@ -76,14 +77,12 @@ class LocationWarningScreen extends StatelessWidget { if (isValid) { Get.offAllNamed(AppRoutes.roleSelection); } else { - Get.snackbar( - 'Location Issue', - 'Your location is still not valid. Please ensure you are in Jember with location services enabled.', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: theme.colorScheme.error.withOpacity( - 0.1, - ), - colorText: theme.colorScheme.error, + TLoaders.errorSnackBar( + title: 'Location Verification Failed', + message: + isMocked + ? 'Please disable mock location apps to continue.' + : 'Ensure you are within Jember region and location services are enabled.', ); } }, diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart index 691024c..27295cc 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart @@ -14,6 +14,9 @@ class OnboardingScreen extends StatelessWidget { // Get the controller final controller = Get.find(); + // Tambahkan controller untuk PageView teks + final textPageController = controller.textPageController; + // Get screen dimensions for responsive design final size = MediaQuery.of(context).size; final isSmallScreen = size.height < 700; @@ -22,105 +25,22 @@ class OnboardingScreen extends StatelessWidget { bool isDark = THelperFunctions.isDarkMode(context); return Scaffold( - body: SafeArea( - child: Column( - children: [ - // Skip button - Align( - alignment: Alignment.topRight, - child: Padding( - padding: const EdgeInsets.all(TSizes.md), - child: TextButton( - onPressed: controller.skipOnboarding, - child: Text( - 'Skip', - style: Theme.of(context).textTheme.bodyMedium, - ), - ), + body: Stack( + children: [ + + // Top bar: indicator & skip + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, ), - ), - - // Page view for slides - Expanded( - flex: 3, - child: PageView.builder( - controller: controller.pageController, - itemCount: contents.length, - onPageChanged: controller.onPageChanged, - itemBuilder: (_, i) { - return Padding( - padding: const EdgeInsets.all(TSizes.lg), - child: Column( - children: [ - // Animated image - Expanded( - flex: isSmallScreen ? 3 : 4, - child: FadeTransition( - opacity: controller.fadeAnimation, - child: SlideTransition( - position: controller.slideAnimation, - child: SvgPicture.asset( - contents[i].image, - fit: BoxFit.contain, - height: isSmallScreen ? 200 : 300, - // Make SVG adapt to dark Theme.of(context) if needed - colorFilter: - isDark - ? const ColorFilter.mode( - Colors.white, - BlendMode.srcIn, - ) - : null, - ), - ), - ), - ), - - SizedBox(height: isSmallScreen ? TSizes.md : TSizes.xl), - - // Animated title - FadeTransition( - opacity: controller.fadeAnimation, - child: SlideTransition( - position: controller.slideAnimation, - child: Text( - contents[i].title, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - ), - - SizedBox(height: isSmallScreen ? TSizes.sm : TSizes.lg), - - // Animated description - FadeTransition( - opacity: controller.fadeAnimation, - child: SlideTransition( - position: controller.slideAnimation, - child: Text( - contents[i].description, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - ], - ), - ); - }, - ), - ), - - // Indicator and buttons - Expanded( - flex: 1, - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Dot indicators Obx( () => Row( - mainAxisAlignment: MainAxisAlignment.center, children: List.generate( contents.length, (index) => buildDot( @@ -132,37 +52,151 @@ class OnboardingScreen extends StatelessWidget { ), ), ), - - const Spacer(), - - // Next button - Padding( - padding: const EdgeInsets.symmetric( - horizontal: TSizes.lg, - vertical: TSizes.md, - ), - child: Align( - alignment: Alignment.centerRight, - child: Container( - margin: const EdgeInsets.only(right: TSizes.sm), - child: FloatingActionButton( - onPressed: controller.nextPage, - backgroundColor: Theme.of(context).primaryColor, - elevation: TSizes.cardElevation, - child: Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.onPrimary, - size: TSizes.iconLg - 2, - ), - ), + TextButton( + onPressed: controller.skipOnboarding, + child: Text( + 'Skip', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), ], ), ), - ], - ), + ), + // Main content + Column( + children: [ + // Spacer for top bar + SizedBox(height: size.height * 0.08), + // Image in the center of gray area + Expanded( + flex: 5, + child: PageView.builder( + controller: controller.pageController, + itemCount: contents.length, + onPageChanged: (i) { + controller.onPageChanged(i); + // Sinkronkan page pada textPageController + textPageController.jumpToPage(i); + }, + itemBuilder: (_, i) { + return Center( + child: FadeTransition( + opacity: controller.fadeAnimation, + child: SlideTransition( + position: controller.slideAnimation, + child: SvgPicture.asset( + contents[i].image, + fit: BoxFit.contain, + height: isSmallScreen ? 200 : 300, + colorFilter: + isDark + ? ColorFilter.mode( + Theme.of(context).colorScheme.onSurface, + BlendMode.srcIn, + ) + : null, + ), + ), + ), + ); + }, + ), + ), + // White rounded bottom sheet tanpa tombol Next + Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + padding: const EdgeInsets.fromLTRB(24, 32, 24, 32), + child: SizedBox( + height: isSmallScreen ? 172 : 212, // dikurangi tinggi tombol + child: PageView.builder( + + controller: textPageController, + physics: const NeverScrollableScrollPhysics(), + itemCount: contents.length, + itemBuilder: (_, i) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + FadeTransition( + opacity: controller.fadeAnimation, + child: SlideTransition( + position: controller.slideAnimation, + child: Text( + contents[i].title, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith(fontWeight: FontWeight.w700), + ), + ), + ), + const SizedBox(height: 12), + // Description + FadeTransition( + opacity: controller.fadeAnimation, + child: SlideTransition( + position: controller.slideAnimation, + child: Text( + contents[i].description, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + // Hapus SizedBox(height: 28) dan tombol Next dari sini + ], + ); + }, + ), + ), + ), + ], + ), + // Tombol Next di posisi paling bawah + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Padding( + padding: const EdgeInsets.fromLTRB( + 24, + 0, + 24, + 16, + ), // kurangi bottom padding agar tidak terlalu mepet + child: SafeArea( + top: false, + minimum: const EdgeInsets.only( + bottom: 8, + ), // beri ruang ekstra agar tidak tertutup gesture bar + child: SizedBox( + width: double.infinity, + height: 56, // tambah tinggi agar text tidak terpotong + child: ElevatedButton( + onPressed: controller.nextPage, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + textStyle: Theme.of(context).textTheme.titleMedium, + ), + child: const Text('Next', overflow: TextOverflow.visible), + ), + ), + ), + ), + ), + ], ), ); } @@ -176,12 +210,12 @@ class OnboardingScreen extends StatelessWidget { ) { return AnimatedContainer( duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.only(right: TSizes.sm), - height: TSizes.xs * 2, - width: currentIndex == index ? TSizes.md + 8 : TSizes.xs * 2, + margin: const EdgeInsets.only(right: 6), + height: 6, + width: currentIndex == index ? 22 : 10, decoration: BoxDecoration( color: currentIndex == index ? activeColor : inactiveColor, - borderRadius: BorderRadius.circular(TSizes.xs), + borderRadius: BorderRadius.circular(8), ), ); } diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart index 0519c29..374df10 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; -import 'package:sigap/src/features/auth/presentasion/widgets/auth_header.dart'; import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart'; -import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/widgets/role_card.dart'; +import 'package:sigap/src/utils/constants/image_strings.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; +import 'package:sigap/src/utils/helpers/helper_functions.dart'; import 'package:sigap/src/utils/loaders/shimmer.dart'; class RoleSelectionScreen extends StatelessWidget { @@ -13,142 +14,475 @@ class RoleSelectionScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // Get the controller final controller = Get.find(); + final isDark = THelperFunctions.isDarkMode(Get.context!); - // Get theme - final theme = Theme.of(context); - - // Set system overlay style SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( + SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: Brightness.dark, + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, ), ); return Scaffold( - backgroundColor: theme.scaffoldBackgroundColor, - appBar: AppBar( - backgroundColor: theme.appBarTheme.backgroundColor, - elevation: 0, - title: Text( - 'Choose Role', - style: Theme.of(context).appBarTheme.titleTextStyle, - ), - centerTitle: true, - ), + backgroundColor: + isDark ? const Color(0xFF18191A) : const Color(0xFFF7F7F5), body: SafeArea( - child: Padding( - padding: const EdgeInsets.all(TSizes.defaultSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - const AuthHeader( - title: 'Select Your Role', - subtitle: 'Choose the role that best describes your position', - ), - - // Role list - Expanded( - child: Obx(() { - // Check if roles are loading - if (controller.isRolesLoading.value) { - // Show shimmer placeholder cards - return ListView.builder( - itemCount: 2, - itemBuilder: - (_, __) => Padding( - padding: const EdgeInsets.only( - bottom: TSizes.spaceBtwItems, + child: Column( + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF232425) : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + isDark + ? const Color(0xFF343536) + : const Color(0xFFE5E5E5), + width: 1, + ), + boxShadow: + isDark + ? [] + : [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 2), ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - TSizes.borderRadiusMd, - ), - border: Border.all(color: theme.dividerColor), + ], + ), + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + // Top illustration that changes based on selection + Expanded( + flex: 3, + child: Obx( + () => _buildTopIllustration(controller, isDark), + ), + ), + const SizedBox(height: 40), + Text( + 'Choose your role', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w700, + color: + isDark ? Colors.white : const Color(0xFF2F2F2F), + letterSpacing: -0.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Obx( + () => Text( + _getSubtitleText(controller, isDark), + style: TextStyle( + fontSize: 16, + color: + isDark + ? Colors.white70 + : const Color(0xFF6B6B6B), + height: 1.5, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 48), + Obx(() { + if (controller.isRolesLoading.value) { + return _buildShimmerCards(isDark); + } else { + return _buildRoleCards(controller, isDark); + } + }), + const SizedBox(height: 48), + Obx( + () => SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: + controller.selectedRole.value != null + ? controller.continueWithRole + : null, + style: ElevatedButton.styleFrom( + backgroundColor: + isDark + ? Colors.white + : const Color(0xFF2F2F2F), + foregroundColor: + isDark ? Colors.black : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - child: Padding( - padding: const EdgeInsets.all(TSizes.md), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Icon shimmer - const TShimmerEffect( - width: 40, - height: 40, - radius: TSizes.borderRadiusSm, - ), - const SizedBox(width: TSizes.md), - // Text shimmer - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - // Role title shimmer - const TShimmerEffect( - width: 150, - height: 24, - radius: TSizes.borderRadiusXs, - ), - const SizedBox(height: TSizes.sm), - - // Role description shimmer - const TShimmerEffect( - width: double.infinity, - height: 16, - radius: TSizes.borderRadiusXs, - ), - const SizedBox(height: TSizes.xs), - const TShimmerEffect( - width: 200, - height: 16, - radius: TSizes.borderRadiusXs, - ), - ], + elevation: 0, + disabledBackgroundColor: + isDark + ? const Color(0xFF343536) + : const Color(0xFFF1F1F1), + disabledForegroundColor: const Color(0xFFB0B0B0), + padding: const EdgeInsets.symmetric( + vertical: 0, + horizontal: 0, + ), // reset padding + minimumSize: const Size(0, 48), // ensure height + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: + controller.isLoading.value + ? SizedBox( + child: CircularProgressIndicator( + color: + isDark + ? Colors.black + : Colors.white, + strokeWidth: 2, + ), + ) + : FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Get started', + style: TextStyle( + fontSize: TSizes.fontSizeMd, + fontWeight: FontWeight.w500, + letterSpacing: 0.1, + ), ), ), - ], - ), - ), - ), ), - ); - } else { - // Show dynamic cards from database - return ListView.builder( - itemCount: controller.roles.length, - itemBuilder: (context, index) { - final role = controller.roles[index]; - return Obx( - () => RoleCard( - role: role, - isSelected: - controller.selectedRole.value?.id == role.id, - onTap: () => controller.selectRole(role), - ), - ); - }, - ); - } - }), - ), - - // Continue button - Obx( - () => AuthButton( - text: 'Continue', - onPressed: controller.continueWithRole, - isLoading: controller.isLoading.value, + ), + ), + ], + ), ), ), + ), + ], + ), + ), + ); + } + + Widget _buildTopIllustration( + RoleSelectionController controller, + bool isDark, + ) { + // Default illustration when no role is selected + if (controller.selectedRole.value == null) { + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF232425) : const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? const Color(0xFF343536) : const Color(0xFFEEEEEE), + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: SvgPicture.asset( + TImages.homeOffice, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + colorFilter: + isDark + ? const ColorFilter.mode(Colors.white70, BlendMode.srcIn) + : null, + ), + ), + ); + } + + // Change illustration based on selected role (viewer/officer only) + final name = controller.selectedRole.value?.name.toLowerCase() ?? ''; + String asset; + bool isSvg = false; + + if (name.contains('officer')) { + asset = isDark ? TImages.customerSupportDark : TImages.customerSupport; + } else { + asset = isDark ? TImages.communication : TImages.communication; + } + + if (asset.toLowerCase().endsWith('.svg')) { + isSvg = true; + } + + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF232425) : const Color(0xFFFAFAFA), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isDark ? const Color(0xFF343536) : const Color(0xFFEEEEEE), + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + isSvg + ? SvgPicture.asset( + asset, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + colorFilter: + isDark + ? const ColorFilter.mode( + Colors.white70, + BlendMode.srcIn, + ) + : null, + ) + : Image.asset( + asset, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + color: isDark ? Colors.white70 : null, + colorBlendMode: isDark ? BlendMode.srcIn : null, + ), + ), + ); + } + + String _getSubtitleText(RoleSelectionController controller, bool isDark) { + if (controller.selectedRole.value == null) { + return isDark + ? 'Select your role to start your journey.\nYou can always change it later.' + : 'Select your role to get started with\nyour learning journey.'; + } + + final selectedRole = controller.selectedRole.value!.name.toLowerCase(); + if (selectedRole.contains('officer')) { + return 'You will manage crime related tasks, handle reports, and ensure the safety of the community.'; + } else { + return 'You have access to panic button for our early warning system. '; + } + } + + Widget _buildRoleCards(RoleSelectionController controller, bool isDark) { + return Row( + children: [ + // First role card + Expanded( + child: Obx( + () => _buildRoleCard( + title: + controller.roles.isNotEmpty + ? controller.roles[0].name + : 'Officer', + isSelected: + controller.selectedRole.value?.id == + (controller.roles.isNotEmpty ? controller.roles[0].id : null), + onTap: + () => + controller.roles.isNotEmpty + ? controller.selectRole(controller.roles[0]) + : null, + illustration: _buildCardOfficerIllustration(isDark), + isDark: isDark, + ), + ), + ), + const SizedBox(width: 16), + // Second role card + Expanded( + child: Obx( + () => _buildRoleCard( + title: + controller.roles.length > 1 + ? controller.roles[1].name + : 'Viewer', + isSelected: + controller.selectedRole.value?.id == + (controller.roles.length > 1 ? controller.roles[1].id : null), + onTap: + () => + controller.roles.length > 1 + ? controller.selectRole(controller.roles[1]) + : null, + illustration: _buildCardViewerIllustration(isDark), + isDark: isDark, + ), + ), + ), + ], + ); + } + + Widget _buildRoleCard({ + required String title, + required bool isSelected, + required VoidCallback? onTap, + required Widget illustration, + required bool isDark, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 140, + decoration: BoxDecoration( + color: + isSelected + ? (isDark ? const Color(0xFF232425) : const Color(0xFFF8F9FA)) + : (isDark ? const Color(0xFF18191A) : Colors.white), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + isSelected + ? (isDark ? Colors.white : const Color(0xFF2F2F2F)) + : (isDark + ? const Color(0xFF343536) + : const Color(0xFFE5E5E5)), + width: isSelected ? 2 : 1, + ), + boxShadow: + isSelected && !isDark + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + illustration, + const SizedBox(height: 16), + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : const Color(0xFF2F2F2F), + letterSpacing: 0.1, + ), + textAlign: TextAlign.center, + ), ], ), ), ), ); } + + Widget _buildCardViewerIllustration(bool isDark) { + return Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: isDark ? const Color(0xFF343536) : const Color(0xFFE0E0E0), + width: 1, + ), + ), + child: Center( + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: isDark ? Colors.white : const Color(0xFF2F2F2F), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + TablerIcons.user_star, + color: isDark ? Colors.black : Colors.white, + size: 14, + ), + ), + ), + ); + } + + Widget _buildCardOfficerIllustration(bool isDark) { + return Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: isDark ? const Color(0xFF343536) : const Color(0xFFE0E0E0), + width: 1, + ), + ), + child: Center( + child: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: isDark ? Colors.white : const Color(0xFF2F2F2F), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + TablerIcons.user, + color: isDark ? Colors.black : Colors.white, + size: 14, + ), + ), + ), + ); + } + + Widget _buildShimmerCards(bool isDark) { + return Row( + children: [ + Expanded( + child: Container( + height: 140, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5), + border: Border.all( + color: + isDark ? const Color(0xFF343536) : const Color(0xFFE5E5E5), + width: 1, + ), + ), + child: const TShimmerEffect( + width: double.infinity, + height: 140, + radius: 8, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Container( + height: 140, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isDark ? const Color(0xFF232425) : const Color(0xFFF5F5F5), + border: Border.all( + color: + isDark ? const Color(0xFF343536) : const Color(0xFFE5E5E5), + width: 1, + ), + ), + child: const TShimmerEffect( + width: double.infinity, + height: 140, + radius: 8, + ), + ), + ), + ], + ); + } } diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart index ae49982..9c5e26e 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart @@ -76,7 +76,7 @@ class WelcomeScreen extends StatelessWidget { frameRate: FrameRate(60), ), ), - + // Bottom flexible space const Spacer(flex: 1), @@ -87,18 +87,12 @@ class WelcomeScreen extends StatelessWidget { child: ElevatedButton( onPressed: controller.getStarted, style: theme.elevatedButtonTheme.style, - child: Text( - 'Get Started', - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - fontWeight: FontWeight.bold, - ), - ), + child: Text('Get Started'), ), ), SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl), Text( - 'By continuing, you agree to our Terms of Service and Privacy Policy', + 'By continuing, We will check your location for our security purposes. Please ensure your location services are enabled.', textAlign: TextAlign.center, style: theme.textTheme.labelMedium, ), diff --git a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart index a4b1815..7688fec 100644 --- a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart +++ b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart @@ -55,7 +55,7 @@ class CustomTextField extends StatelessWidget { // Determine the effective fill color final Color effectiveFillColor = - fillColor ?? (isDark ? TColors.darkContainer : TColors.lightContainer); + fillColor ?? (isDark ? TColors.dark : TColors.lightContainer); // Get the common input decoration for both cases final inputDecoration = _getInputDecoration( diff --git a/sigap-mobile/lib/src/utils/constants/colors.dart b/sigap-mobile/lib/src/utils/constants/colors.dart index 717ab21..4016b11 100644 --- a/sigap-mobile/lib/src/utils/constants/colors.dart +++ b/sigap-mobile/lib/src/utils/constants/colors.dart @@ -1,66 +1,38 @@ import 'package:flutter/material.dart'; class TColors { - // App theme colors - static const Color primary = Color( - 0xFF3B82F6, - ); // From lightThemeColors primary - static const Color secondary = Color( - 0xFFF1F5F9, - ); // From lightThemeColors secondary - static const Color accent = Color(0xFFF1F5F9); // From lightThemeColors accent + // Monochrome Notion-like theme colors + static const Color primary = Color(0xFF2F2F2F); // Notion dark text + static const Color secondary = Color(0xFFF5F5F5); // Notion light bg + static const Color accent = Color(0xFFFAFAFA); // Text colors - static const Color textPrimary = Color( - 0xFF0B0C1E, - ); // From lightThemeColors foreground - static const Color textSecondary = Color( - 0xFF707C91, - ); // From lightThemeColors mutedForeground + static const Color textPrimary = Color(0xFF2F2F2F); + static const Color textSecondary = Color(0xFF6B6B6B); static const Color textWhite = Colors.white; // Background colors - static const Color light = Color( - 0xFFFFFFFF, - ); // From lightThemeColors background - static const Color dark = Color( - 0xFF0B0C1E, - ); // From darkThemeColors background - static const Color primaryBackground = Color( - 0xFFFFFFFF, - ); // From lightThemeColors background + static const Color light = Color(0xFFF7F7F5); // Notion off-white + static const Color dark = Color(0xFF18191A); // Notion dark mode bg + static const Color primaryBackground = Color(0xFFF7F7F5); // Background Container colors - static const Color lightContainer = Color( - 0xFFFFFFFF, - ); // From lightThemeColors card - static Color darkContainer = Color(0xFF0B0C1E); // From darkThemeColors card + static const Color lightContainer = Color(0xFFFFFFFF); + static const Color darkContainer = Color(0xFF232425); // Button colors - static const Color buttonPrimary = Color( - 0xFF3B82F6, - ); // From lightThemeColors primary - static const Color buttonSecondary = Color( - 0xFFF1F5F9, - ); // From lightThemeColors secondary - static const Color buttonDisabled = Color( - 0xFF707C91, - ); // From lightThemeColors mutedForeground + static const Color buttonPrimary = Color(0xFF2F2F2F); + static const Color buttonSecondary = Color(0xFFF5F5F5); + static const Color buttonDisabled = Color(0xFFB0B0B0); // Border colors - static const Color borderPrimary = Color( - 0xFFE2E8F0, - ); // From lightThemeColors border - static const Color borderSecondary = Color( - 0xFFE2E8F0, - ); // From lightThemeColors input + static const Color borderPrimary = Color(0xFFE5E5E5); + static const Color borderSecondary = Color(0xFFE5E5E5); // Error and validation colors - static const Color error = Color( - 0xFFEF4444, - ); // From lightThemeColors destructive - static const Color success = Color(0xFF38B2AC); // From darkThemeColors chart2 - static const Color warning = Color(0xFFF59E0B); // From darkThemeColors chart3 + static const Color error = Color(0xFFEF4444); + static const Color success = Color(0xFF38B2AC); + static const Color warning = Color(0xFFF59E0B); // Neutral Shades static const Color black = Color(0xFF232323); @@ -74,58 +46,34 @@ class TColors { // Additional colors static const Color transparent = Colors.transparent; - // Theme color maps for reference + // Theme color maps for reference (monochrome) static const Map darkThemeColors = { - 'background': Color(0xFF0B0C1E), - 'foreground': Color(0xFFF8FAFC), - 'card': Color(0xFF0B0C1E), - 'cardForeground': Color(0xFFF8FAFC), - 'popover': Color(0xFF0B0C1E), - 'popoverForeground': Color(0xFFF8FAFC), - 'primary': Color(0xFF2563EB), - 'primaryForeground': Color(0xFF121826), - 'secondary': Color(0xFF1E293B), - 'secondaryForeground': Color(0xFFF8FAFC), - 'muted': Color(0xFF1E293B), - 'mutedForeground': Color(0xFFA0AEC0), - 'accent': Color(0xFF1E293B), - 'accentForeground': Color(0xFFF8FAFC), - 'destructive': Color(0xFF752727), - 'destructiveForeground': Color(0xFFF8FAFC), - 'border': Color(0xFF1E293B), - 'input': Color(0xFF1E293B), - 'ring': Color(0xFF3B5FE8), - 'chart1': Color(0xFF3B82F6), - 'chart2': Color(0xFF38B2AC), - 'chart3': Color(0xFFF59E0B), - 'chart4': Color(0xFFC084FC), - 'chart5': Color(0xFFEF476F), + 'background': Color(0xFF18191A), + 'foreground': Colors.white, + 'card': Color(0xFF232425), + 'cardForeground': Colors.white, + 'primary': Color(0xFF2F2F2F), + 'secondary': Color(0xFF232425), + 'muted': Color(0xFF343536), + 'mutedForeground': Color(0xFFB0B0B0), + 'accent': Color(0xFF343536), + 'destructive': Color(0xFFEF4444), + 'border': Color(0xFF343536), + 'input': Color(0xFF343536), }; static const Map lightThemeColors = { - 'background': Color(0xFFFFFFFF), - 'foreground': Color(0xFF0B0C1E), + 'background': Color(0xFFF7F7F5), + 'foreground': Color(0xFF2F2F2F), 'card': Color(0xFFFFFFFF), - 'cardForeground': Color(0xFF0B0C1E), - 'popover': Color(0xFFFFFFFF), - 'popoverForeground': Color(0xFF0B0C1E), - 'primary': Color(0xFF3B82F6), - 'primaryForeground': Color(0xFFF8FAFC), - 'secondary': Color(0xFFF1F5F9), - 'secondaryForeground': Color(0xFF121826), - 'muted': Color(0xFFF1F5F9), - 'mutedForeground': Color(0xFF707C91), - 'accent': Color(0xFFF1F5F9), - 'accentForeground': Color(0xFF121826), + 'cardForeground': Color(0xFF2F2F2F), + 'primary': Color(0xFF2F2F2F), + 'secondary': Color(0xFFF5F5F5), + 'muted': Color(0xFFF1F1F1), + 'mutedForeground': Color(0xFFB0B0B0), + 'accent': Color(0xFFFAFAFA), 'destructive': Color(0xFFEF4444), - 'destructiveForeground': Color(0xFFF8FAFC), - 'border': Color(0xFFE2E8F0), - 'input': Color(0xFFE2E8F0), - 'ring': Color(0xFF3B82F6), - 'chart1': Color(0xFFED6A43), - 'chart2': Color(0xFF2A9D8F), - 'chart3': Color(0xFF2A495B), - 'chart4': Color(0xFFF4C15D), - 'chart5': Color(0xFFF98F3D), + 'border': Color(0xFFE5E5E5), + 'input': Color(0xFFE5E5E5), }; } diff --git a/sigap-mobile/lib/src/utils/popups/circular_full_screen_loader.dart b/sigap-mobile/lib/src/utils/popups/circular_full_screen_loader.dart new file mode 100644 index 0000000..8adbefc --- /dev/null +++ b/sigap-mobile/lib/src/utils/popups/circular_full_screen_loader.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:sigap/src/utils/loaders/circular_loader.dart'; + +import '../constants/colors.dart'; +import '../helpers/helper_functions.dart'; + +/// A utility class for managing a full-screen loading dialog. +class TCircularFullScreenLoader { + /// Show a full-screen loader with optional [text] below the spinner. + static void openLoadingDialog({String? text}) { + final isDark = THelperFunctions.isDarkMode(Get.context!); + WidgetsBinding.instance.addPostFrameCallback((_) { + showDialog( + context: Get.overlayContext!, + barrierDismissible: false, + builder: + (_) => PopScope( + canPop: false, + child: Container( + color: isDark ? TColors.black : TColors.white, + width: double.infinity, + height: double.infinity, + child: Center( + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TCircularLoader( + foregroundColor: TColors.primary, + backgroundColor: + isDark ? TColors.black : TColors.white, + ), + if (text != null && text.isNotEmpty) ...[ + const SizedBox(height: 24), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 220), + child: Text( + text, + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.white : Colors.black, + fontWeight: FontWeight.w500, + decoration: + TextDecoration.none, // Hapus underline + ), + textAlign: TextAlign.center, + softWrap: true, + overflow: TextOverflow.visible, + ), + ), + ], + ], + ), + ), + ), + ), + ), + ); + }); + } + + /// Stop the currently open loading dialog. + static stopLoading() { + Navigator.of(Get.overlayContext!).pop(); + } +} diff --git a/sigap-mobile/lib/src/utils/popups/full_screen_loader.dart b/sigap-mobile/lib/src/utils/popups/full_screen_loader.dart index 704eec6..0c391f2 100644 --- a/sigap-mobile/lib/src/utils/popups/full_screen_loader.dart +++ b/sigap-mobile/lib/src/utils/popups/full_screen_loader.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; + import '../constants/colors.dart'; import '../helpers/helper_functions.dart'; import '../loaders/animation_loader.dart';