Refactor onboarding and authentication UI components
- Updated password field to use a consistent dark color. - Enhanced onboarding controller with a new text page controller for synchronized page views. - Improved location warning screen to utilize a centralized error snackbar. - Refined onboarding screen layout for better responsiveness and added text synchronization. - Revamped role selection screen with improved UI and dynamic role card rendering. - Updated welcome screen text for clarity on location services. - Adjusted custom text field to align with new color scheme. - Simplified color constants for a more cohesive theme. - Introduced a new circular full-screen loader for better loading experience.
This commit is contained in:
parent
1a6eefe6e3
commit
6a4813c15e
|
@ -138,7 +138,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
bool biometricSuccess = await attemptBiometricLogin();
|
bool biometricSuccess = await attemptBiometricLogin();
|
||||||
if (!biometricSuccess) {
|
if (!biometricSuccess) {
|
||||||
if (isFirstTime) {
|
if (isFirstTime) {
|
||||||
_navigateToRoute(AppRoutes.signIn);
|
_navigateToRoute(AppRoutes.onboarding);
|
||||||
} else {
|
} else {
|
||||||
_navigateToRoute(AppRoutes.onboarding);
|
_navigateToRoute(AppRoutes.onboarding);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signin/signin_controller.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/password_field.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.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/shared/widgets/text/custom_text_field.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class SignInScreen extends StatelessWidget {
|
class SignInScreen extends StatelessWidget {
|
||||||
|
@ -20,23 +20,13 @@ class SignInScreen extends StatelessWidget {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// Get the controller - use Get.put to ensure it's initialized
|
// Get the controller - use Get.put to ensure it's initialized
|
||||||
final controller = Get.put(SignInController());
|
final controller = Get.find<SignInController>();
|
||||||
|
|
||||||
// Check if dark mode is enabled
|
// Check if dark mode is enabled
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
final isDarkMode = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Set system overlay style based on theme
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
SystemUiOverlayStyle(
|
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
statusBarIconBrightness:
|
|
||||||
isDarkMode ? Brightness.light : Brightness.dark,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// Use dynamic background color from theme
|
// Use dynamic background color from theme
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
|
@ -33,7 +33,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: dark ? TColors.darkContainer : TColors.lightContainer,
|
backgroundColor: dark ? TColors.dark : TColors.lightContainer,
|
||||||
appBar: _buildAppBar(context, dark),
|
appBar: _buildAppBar(context, dark),
|
||||||
body: Obx(() {
|
body: Obx(() {
|
||||||
// Show loading state while controller initializes
|
// Show loading state while controller initializes
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
@ -25,14 +24,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Set system overlay style
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
SystemUiOverlayStyle(
|
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Obx(
|
body: Obx(
|
||||||
() => NestedScrollView(
|
() => NestedScrollView(
|
||||||
|
@ -223,7 +214,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.05),
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
@ -245,7 +236,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -511,7 +502,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
text: 'Google',
|
text: 'Google',
|
||||||
iconImage: TImages.googleIcon,
|
iconImage: TImages.googleIcon,
|
||||||
onPressed: () => controller.signInWithGoogle(),
|
onPressed: () => controller.signInWithGoogle(),
|
||||||
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
|
backgroundColor: isDark ? TColors.dark : Colors.white,
|
||||||
foregroundColor: isDark ? TColors.white : TColors.dark,
|
foregroundColor: isDark ? TColors.white : TColors.dark,
|
||||||
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
||||||
),
|
),
|
||||||
|
@ -523,7 +514,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
text: 'Facebook',
|
text: 'Facebook',
|
||||||
iconImage: TImages.facebookIcon,
|
iconImage: TImages.facebookIcon,
|
||||||
onPressed: () => controller.signInWithFacebook(),
|
onPressed: () => controller.signInWithFacebook(),
|
||||||
backgroundColor: isDark ? TColors.darkContainer : Colors.white,
|
backgroundColor: isDark ? TColors.dark : Colors.white,
|
||||||
foregroundColor: isDark ? TColors.white : TColors.dark,
|
foregroundColor: isDark ? TColors.white : TColors.dark,
|
||||||
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
borderColor: isDark ? Colors.transparent : Colors.grey.shade300,
|
||||||
),
|
),
|
||||||
|
|
|
@ -82,7 +82,7 @@ class PasswordField extends StatelessWidget {
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor:
|
fillColor:
|
||||||
isDark ? TColors.darkContainer : TColors.lightContainer,
|
isDark ? TColors.dark : TColors.lightContainer,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.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/cores/services/location_service.dart';
|
||||||
import 'package:sigap/src/features/onboarding/data/dummy/onboarding_dummy.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/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/popups/circular_full_screen_loader.dart';
|
||||||
import 'package:sigap/src/utils/popups/full_screen_loader.dart';
|
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
|
||||||
|
|
||||||
class OnboardingController extends GetxController
|
class OnboardingController extends GetxController
|
||||||
with GetSingleTickerProviderStateMixin {
|
with GetSingleTickerProviderStateMixin {
|
||||||
|
@ -23,6 +20,8 @@ class OnboardingController extends GetxController
|
||||||
// Observable variables
|
// Observable variables
|
||||||
final RxInt currentIndex = 0.obs;
|
final RxInt currentIndex = 0.obs;
|
||||||
final PageController pageController = PageController(initialPage: 0);
|
final PageController pageController = PageController(initialPage: 0);
|
||||||
|
// Tambahkan controller untuk PageView teks
|
||||||
|
final PageController textPageController = PageController(initialPage: 0);
|
||||||
final RxBool isLocationChecking = false.obs;
|
final RxBool isLocationChecking = false.obs;
|
||||||
|
|
||||||
// Animation controllers
|
// Animation controllers
|
||||||
|
@ -57,6 +56,7 @@ class OnboardingController extends GetxController
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
pageController.dispose();
|
pageController.dispose();
|
||||||
|
textPageController.dispose(); // dispose controller baru
|
||||||
animationController.dispose();
|
animationController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ class OnboardingController extends GetxController
|
||||||
currentIndex.value = index;
|
currentIndex.value = index;
|
||||||
animationController.reset();
|
animationController.reset();
|
||||||
animationController.forward();
|
animationController.forward();
|
||||||
|
// Sinkronisasi textPageController dilakukan di widget, tidak perlu di sini
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to go to next page
|
// Method to go to next page
|
||||||
|
@ -97,53 +98,40 @@ class OnboardingController extends GetxController
|
||||||
isLocationChecking.value = true;
|
isLocationChecking.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
TCircularFullScreenLoader.openLoadingDialog(text: 'Checking location...');
|
||||||
TFullScreenLoader.openLoadingDialog(
|
|
||||||
'Checking location...',
|
|
||||||
TImages.loader,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify location is valid (in Jember and not mocked)
|
// Verify location is valid (in Jember and not mocked)
|
||||||
final isLocationValid =
|
final isLocationValid =
|
||||||
await _locationService.isLocationValidForFeature();
|
await _locationService.isLocationValidForFeature();
|
||||||
|
|
||||||
TFullScreenLoader.stopLoading();
|
TCircularFullScreenLoader.stopLoading();
|
||||||
|
|
||||||
Logger().i('isFirstTime before: ${storage.read('isFirstTime')}');
|
|
||||||
|
|
||||||
storage.write('isFirstTime', false);
|
storage.write('isFirstTime', false);
|
||||||
|
|
||||||
Logger().i('isFirstTime after: ${storage.read('isFirstTime')}');
|
|
||||||
|
|
||||||
if (isLocationValid) {
|
if (isLocationValid) {
|
||||||
// If location is valid, proceed to role selection
|
// If location is valid, proceed to role selection
|
||||||
|
|
||||||
Get.offAllNamed(AppRoutes.signupWithRole);
|
Get.offAllNamed(AppRoutes.roleSelection);
|
||||||
|
|
||||||
// TLoaders.successSnackBar(
|
|
||||||
// title: 'Location Valid',
|
|
||||||
// message: 'Checking location was successful',
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Store isfirstTime to false in storage
|
// Store isfirstTime to false in storage
|
||||||
} else {
|
} else {
|
||||||
// If location is invalid, show warning screen
|
// 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);
|
Get.offAllNamed(AppRoutes.locationWarning);
|
||||||
|
|
||||||
TLoaders.errorSnackBar(
|
|
||||||
title: 'Location Invalid',
|
|
||||||
message:
|
|
||||||
'Please enable location services or ensure you are in Jember area',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If there's an error, show the location warning screen
|
// If there's an error, show the location warning screen
|
||||||
// TFullScreenLoader.stopLoading();
|
// TFullScreenLoader.stopLoading();
|
||||||
Get.offAllNamed(AppRoutes.locationWarning);
|
Get.offAllNamed(AppRoutes.locationWarning);
|
||||||
} finally {
|
} finally {
|
||||||
isLocationChecking.value = false;
|
isLocationChecking.value = false;
|
||||||
// TFullScreenLoader.stopLoading();
|
// TCircularFullScreenLoader.stopLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
class LocationWarningScreen extends StatelessWidget {
|
class LocationWarningScreen extends StatelessWidget {
|
||||||
const LocationWarningScreen({super.key});
|
const LocationWarningScreen({super.key});
|
||||||
|
@ -76,14 +77,12 @@ class LocationWarningScreen extends StatelessWidget {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
Get.offAllNamed(AppRoutes.roleSelection);
|
Get.offAllNamed(AppRoutes.roleSelection);
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(
|
TLoaders.errorSnackBar(
|
||||||
'Location Issue',
|
title: 'Location Verification Failed',
|
||||||
'Your location is still not valid. Please ensure you are in Jember with location services enabled.',
|
message:
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
isMocked
|
||||||
backgroundColor: theme.colorScheme.error.withOpacity(
|
? 'Please disable mock location apps to continue.'
|
||||||
0.1,
|
: 'Ensure you are within Jember region and location services are enabled.',
|
||||||
),
|
|
||||||
colorText: theme.colorScheme.error,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,9 @@ class OnboardingScreen extends StatelessWidget {
|
||||||
// Get the controller
|
// Get the controller
|
||||||
final controller = Get.find<OnboardingController>();
|
final controller = Get.find<OnboardingController>();
|
||||||
|
|
||||||
|
// Tambahkan controller untuk PageView teks
|
||||||
|
final textPageController = controller.textPageController;
|
||||||
|
|
||||||
// Get screen dimensions for responsive design
|
// Get screen dimensions for responsive design
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
final isSmallScreen = size.height < 700;
|
final isSmallScreen = size.height < 700;
|
||||||
|
@ -22,105 +25,22 @@ class OnboardingScreen extends StatelessWidget {
|
||||||
bool isDark = THelperFunctions.isDarkMode(context);
|
bool isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: Stack(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
|
||||||
// Skip button
|
// Top bar: indicator & skip
|
||||||
Align(
|
SafeArea(
|
||||||
alignment: Alignment.topRight,
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(
|
||||||
padding: const EdgeInsets.all(TSizes.md),
|
horizontal: TSizes.md,
|
||||||
child: TextButton(
|
vertical: TSizes.md,
|
||||||
onPressed: controller.skipOnboarding,
|
|
||||||
child: Text(
|
|
||||||
'Skip',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
// 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(
|
|
||||||
children: [
|
children: [
|
||||||
// Dot indicators
|
// Dot indicators
|
||||||
Obx(
|
Obx(
|
||||||
() => Row(
|
() => Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
contents.length,
|
contents.length,
|
||||||
(index) => buildDot(
|
(index) => buildDot(
|
||||||
|
@ -132,37 +52,151 @@ class OnboardingScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
TextButton(
|
||||||
const Spacer(),
|
onPressed: controller.skipOnboarding,
|
||||||
|
child: Text(
|
||||||
// Next button
|
'Skip',
|
||||||
Padding(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
padding: const EdgeInsets.symmetric(
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
// 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(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
margin: const EdgeInsets.only(right: TSizes.sm),
|
margin: const EdgeInsets.only(right: 6),
|
||||||
height: TSizes.xs * 2,
|
height: 6,
|
||||||
width: currentIndex == index ? TSizes.md + 8 : TSizes.xs * 2,
|
width: currentIndex == index ? 22 : 10,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: currentIndex == index ? activeColor : inactiveColor,
|
color: currentIndex == index ? activeColor : inactiveColor,
|
||||||
borderRadius: BorderRadius.circular(TSizes.xs),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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: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/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/constants/sizes.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
||||||
|
|
||||||
class RoleSelectionScreen extends StatelessWidget {
|
class RoleSelectionScreen extends StatelessWidget {
|
||||||
|
@ -13,142 +14,475 @@ class RoleSelectionScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Get the controller
|
|
||||||
final controller = Get.find<RoleSelectionController>();
|
final controller = Get.find<RoleSelectionController>();
|
||||||
|
final isDark = THelperFunctions.isDarkMode(Get.context!);
|
||||||
|
|
||||||
// Get theme
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
// Set system overlay style
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
statusBarColor: Colors.transparent,
|
statusBarColor: Colors.transparent,
|
||||||
statusBarIconBrightness: Brightness.dark,
|
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor:
|
||||||
appBar: AppBar(
|
isDark ? const Color(0xFF18191A) : const Color(0xFFF7F7F5),
|
||||||
backgroundColor: theme.appBarTheme.backgroundColor,
|
|
||||||
elevation: 0,
|
|
||||||
title: Text(
|
|
||||||
'Choose Role',
|
|
||||||
style: Theme.of(context).appBarTheme.titleTextStyle,
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
children: [
|
||||||
child: Column(
|
Expanded(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Container(
|
||||||
children: [
|
margin: const EdgeInsets.all(16),
|
||||||
// Header
|
decoration: BoxDecoration(
|
||||||
const AuthHeader(
|
color: isDark ? const Color(0xFF232425) : Colors.white,
|
||||||
title: 'Select Your Role',
|
borderRadius: BorderRadius.circular(12),
|
||||||
subtitle: 'Choose the role that best describes your position',
|
border: Border.all(
|
||||||
),
|
color:
|
||||||
|
isDark
|
||||||
// Role list
|
? const Color(0xFF343536)
|
||||||
Expanded(
|
: const Color(0xFFE5E5E5),
|
||||||
child: Obx(() {
|
width: 1,
|
||||||
// Check if roles are loading
|
),
|
||||||
if (controller.isRolesLoading.value) {
|
boxShadow:
|
||||||
// Show shimmer placeholder cards
|
isDark
|
||||||
return ListView.builder(
|
? []
|
||||||
itemCount: 2,
|
: [
|
||||||
itemBuilder:
|
BoxShadow(
|
||||||
(_, __) => Padding(
|
color: Colors.black.withOpacity(0.04),
|
||||||
padding: const EdgeInsets.only(
|
blurRadius: 8,
|
||||||
bottom: TSizes.spaceBtwItems,
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
child: Container(
|
],
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
borderRadius: BorderRadius.circular(
|
child: Padding(
|
||||||
TSizes.borderRadiusMd,
|
padding: const EdgeInsets.all(32),
|
||||||
),
|
child: Column(
|
||||||
border: Border.all(color: theme.dividerColor),
|
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(
|
elevation: 0,
|
||||||
padding: const EdgeInsets.all(TSizes.md),
|
disabledBackgroundColor:
|
||||||
child: Row(
|
isDark
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
? const Color(0xFF343536)
|
||||||
children: [
|
: const Color(0xFFF1F1F1),
|
||||||
// Icon shimmer
|
disabledForegroundColor: const Color(0xFFB0B0B0),
|
||||||
const TShimmerEffect(
|
padding: const EdgeInsets.symmetric(
|
||||||
width: 40,
|
vertical: 0,
|
||||||
height: 40,
|
horizontal: 0,
|
||||||
radius: TSizes.borderRadiusSm,
|
), // reset padding
|
||||||
),
|
minimumSize: const Size(0, 48), // ensure height
|
||||||
const SizedBox(width: TSizes.md),
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
// Text shimmer
|
),
|
||||||
Expanded(
|
child:
|
||||||
child: Column(
|
controller.isLoading.value
|
||||||
crossAxisAlignment:
|
? SizedBox(
|
||||||
CrossAxisAlignment.start,
|
child: CircularProgressIndicator(
|
||||||
children: [
|
color:
|
||||||
// Role title shimmer
|
isDark
|
||||||
const TShimmerEffect(
|
? Colors.black
|
||||||
width: 150,
|
: Colors.white,
|
||||||
height: 24,
|
strokeWidth: 2,
|
||||||
radius: TSizes.borderRadiusXs,
|
),
|
||||||
),
|
)
|
||||||
const SizedBox(height: TSizes.sm),
|
: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
// Role description shimmer
|
child: Text(
|
||||||
const TShimmerEffect(
|
'Get started',
|
||||||
width: double.infinity,
|
style: TextStyle(
|
||||||
height: 16,
|
fontSize: TSizes.fontSizeMd,
|
||||||
radius: TSizes.borderRadiusXs,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
letterSpacing: 0.1,
|
||||||
const SizedBox(height: TSizes.xs),
|
),
|
||||||
const TShimmerEffect(
|
|
||||||
width: 200,
|
|
||||||
height: 16,
|
|
||||||
radius: TSizes.borderRadiusXs,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,18 +87,12 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: controller.getStarted,
|
onPressed: controller.getStarted,
|
||||||
style: theme.elevatedButtonTheme.style,
|
style: theme.elevatedButtonTheme.style,
|
||||||
child: Text(
|
child: Text('Get Started'),
|
||||||
'Get Started',
|
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
||||||
Text(
|
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,
|
textAlign: TextAlign.center,
|
||||||
style: theme.textTheme.labelMedium,
|
style: theme.textTheme.labelMedium,
|
||||||
),
|
),
|
||||||
|
|
|
@ -55,7 +55,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
|
|
||||||
// Determine the effective fill color
|
// Determine the effective fill color
|
||||||
final Color effectiveFillColor =
|
final Color effectiveFillColor =
|
||||||
fillColor ?? (isDark ? TColors.darkContainer : TColors.lightContainer);
|
fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
||||||
|
|
||||||
// Get the common input decoration for both cases
|
// Get the common input decoration for both cases
|
||||||
final inputDecoration = _getInputDecoration(
|
final inputDecoration = _getInputDecoration(
|
||||||
|
|
|
@ -1,66 +1,38 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TColors {
|
class TColors {
|
||||||
// App theme colors
|
// Monochrome Notion-like theme colors
|
||||||
static const Color primary = Color(
|
static const Color primary = Color(0xFF2F2F2F); // Notion dark text
|
||||||
0xFF3B82F6,
|
static const Color secondary = Color(0xFFF5F5F5); // Notion light bg
|
||||||
); // From lightThemeColors primary
|
static const Color accent = Color(0xFFFAFAFA);
|
||||||
static const Color secondary = Color(
|
|
||||||
0xFFF1F5F9,
|
|
||||||
); // From lightThemeColors secondary
|
|
||||||
static const Color accent = Color(0xFFF1F5F9); // From lightThemeColors accent
|
|
||||||
|
|
||||||
// Text colors
|
// Text colors
|
||||||
static const Color textPrimary = Color(
|
static const Color textPrimary = Color(0xFF2F2F2F);
|
||||||
0xFF0B0C1E,
|
static const Color textSecondary = Color(0xFF6B6B6B);
|
||||||
); // From lightThemeColors foreground
|
|
||||||
static const Color textSecondary = Color(
|
|
||||||
0xFF707C91,
|
|
||||||
); // From lightThemeColors mutedForeground
|
|
||||||
static const Color textWhite = Colors.white;
|
static const Color textWhite = Colors.white;
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
static const Color light = Color(
|
static const Color light = Color(0xFFF7F7F5); // Notion off-white
|
||||||
0xFFFFFFFF,
|
static const Color dark = Color(0xFF18191A); // Notion dark mode bg
|
||||||
); // From lightThemeColors background
|
static const Color primaryBackground = Color(0xFFF7F7F5);
|
||||||
static const Color dark = Color(
|
|
||||||
0xFF0B0C1E,
|
|
||||||
); // From darkThemeColors background
|
|
||||||
static const Color primaryBackground = Color(
|
|
||||||
0xFFFFFFFF,
|
|
||||||
); // From lightThemeColors background
|
|
||||||
|
|
||||||
// Background Container colors
|
// Background Container colors
|
||||||
static const Color lightContainer = Color(
|
static const Color lightContainer = Color(0xFFFFFFFF);
|
||||||
0xFFFFFFFF,
|
static const Color darkContainer = Color(0xFF232425);
|
||||||
); // From lightThemeColors card
|
|
||||||
static Color darkContainer = Color(0xFF0B0C1E); // From darkThemeColors card
|
|
||||||
|
|
||||||
// Button colors
|
// Button colors
|
||||||
static const Color buttonPrimary = Color(
|
static const Color buttonPrimary = Color(0xFF2F2F2F);
|
||||||
0xFF3B82F6,
|
static const Color buttonSecondary = Color(0xFFF5F5F5);
|
||||||
); // From lightThemeColors primary
|
static const Color buttonDisabled = Color(0xFFB0B0B0);
|
||||||
static const Color buttonSecondary = Color(
|
|
||||||
0xFFF1F5F9,
|
|
||||||
); // From lightThemeColors secondary
|
|
||||||
static const Color buttonDisabled = Color(
|
|
||||||
0xFF707C91,
|
|
||||||
); // From lightThemeColors mutedForeground
|
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
static const Color borderPrimary = Color(
|
static const Color borderPrimary = Color(0xFFE5E5E5);
|
||||||
0xFFE2E8F0,
|
static const Color borderSecondary = Color(0xFFE5E5E5);
|
||||||
); // From lightThemeColors border
|
|
||||||
static const Color borderSecondary = Color(
|
|
||||||
0xFFE2E8F0,
|
|
||||||
); // From lightThemeColors input
|
|
||||||
|
|
||||||
// Error and validation colors
|
// Error and validation colors
|
||||||
static const Color error = Color(
|
static const Color error = Color(0xFFEF4444);
|
||||||
0xFFEF4444,
|
static const Color success = Color(0xFF38B2AC);
|
||||||
); // From lightThemeColors destructive
|
static const Color warning = Color(0xFFF59E0B);
|
||||||
static const Color success = Color(0xFF38B2AC); // From darkThemeColors chart2
|
|
||||||
static const Color warning = Color(0xFFF59E0B); // From darkThemeColors chart3
|
|
||||||
|
|
||||||
// Neutral Shades
|
// Neutral Shades
|
||||||
static const Color black = Color(0xFF232323);
|
static const Color black = Color(0xFF232323);
|
||||||
|
@ -74,58 +46,34 @@ class TColors {
|
||||||
// Additional colors
|
// Additional colors
|
||||||
static const Color transparent = Colors.transparent;
|
static const Color transparent = Colors.transparent;
|
||||||
|
|
||||||
// Theme color maps for reference
|
// Theme color maps for reference (monochrome)
|
||||||
static const Map<String, Color> darkThemeColors = {
|
static const Map<String, Color> darkThemeColors = {
|
||||||
'background': Color(0xFF0B0C1E),
|
'background': Color(0xFF18191A),
|
||||||
'foreground': Color(0xFFF8FAFC),
|
'foreground': Colors.white,
|
||||||
'card': Color(0xFF0B0C1E),
|
'card': Color(0xFF232425),
|
||||||
'cardForeground': Color(0xFFF8FAFC),
|
'cardForeground': Colors.white,
|
||||||
'popover': Color(0xFF0B0C1E),
|
'primary': Color(0xFF2F2F2F),
|
||||||
'popoverForeground': Color(0xFFF8FAFC),
|
'secondary': Color(0xFF232425),
|
||||||
'primary': Color(0xFF2563EB),
|
'muted': Color(0xFF343536),
|
||||||
'primaryForeground': Color(0xFF121826),
|
'mutedForeground': Color(0xFFB0B0B0),
|
||||||
'secondary': Color(0xFF1E293B),
|
'accent': Color(0xFF343536),
|
||||||
'secondaryForeground': Color(0xFFF8FAFC),
|
'destructive': Color(0xFFEF4444),
|
||||||
'muted': Color(0xFF1E293B),
|
'border': Color(0xFF343536),
|
||||||
'mutedForeground': Color(0xFFA0AEC0),
|
'input': Color(0xFF343536),
|
||||||
'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),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static const Map<String, Color> lightThemeColors = {
|
static const Map<String, Color> lightThemeColors = {
|
||||||
'background': Color(0xFFFFFFFF),
|
'background': Color(0xFFF7F7F5),
|
||||||
'foreground': Color(0xFF0B0C1E),
|
'foreground': Color(0xFF2F2F2F),
|
||||||
'card': Color(0xFFFFFFFF),
|
'card': Color(0xFFFFFFFF),
|
||||||
'cardForeground': Color(0xFF0B0C1E),
|
'cardForeground': Color(0xFF2F2F2F),
|
||||||
'popover': Color(0xFFFFFFFF),
|
'primary': Color(0xFF2F2F2F),
|
||||||
'popoverForeground': Color(0xFF0B0C1E),
|
'secondary': Color(0xFFF5F5F5),
|
||||||
'primary': Color(0xFF3B82F6),
|
'muted': Color(0xFFF1F1F1),
|
||||||
'primaryForeground': Color(0xFFF8FAFC),
|
'mutedForeground': Color(0xFFB0B0B0),
|
||||||
'secondary': Color(0xFFF1F5F9),
|
'accent': Color(0xFFFAFAFA),
|
||||||
'secondaryForeground': Color(0xFF121826),
|
|
||||||
'muted': Color(0xFFF1F5F9),
|
|
||||||
'mutedForeground': Color(0xFF707C91),
|
|
||||||
'accent': Color(0xFFF1F5F9),
|
|
||||||
'accentForeground': Color(0xFF121826),
|
|
||||||
'destructive': Color(0xFFEF4444),
|
'destructive': Color(0xFFEF4444),
|
||||||
'destructiveForeground': Color(0xFFF8FAFC),
|
'border': Color(0xFFE5E5E5),
|
||||||
'border': Color(0xFFE2E8F0),
|
'input': Color(0xFFE5E5E5),
|
||||||
'input': Color(0xFFE2E8F0),
|
|
||||||
'ring': Color(0xFF3B82F6),
|
|
||||||
'chart1': Color(0xFFED6A43),
|
|
||||||
'chart2': Color(0xFF2A9D8F),
|
|
||||||
'chart3': Color(0xFF2A495B),
|
|
||||||
'chart4': Color(0xFFF4C15D),
|
|
||||||
'chart5': Color(0xFFF98F3D),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../constants/colors.dart';
|
import '../constants/colors.dart';
|
||||||
import '../helpers/helper_functions.dart';
|
import '../helpers/helper_functions.dart';
|
||||||
import '../loaders/animation_loader.dart';
|
import '../loaders/animation_loader.dart';
|
||||||
|
|
Loading…
Reference in New Issue