feat: Enhance officer information signup form with additional fields and validation
- Added new text controllers and error states for unit ID, patrol unit ID, name, position, phone, email, valid until, avatar, QR code, banned reason, and banned until. - Implemented validation logic for the new fields in the officer info controller. - Updated the signup form screen to include new input fields for unit ID and patrol unit ID. - Refactored the signup with role screen to improve layout and remove unnecessary components. - Introduced a new role signup page view to manage role selection and signup process. - Updated the welcome screen button text to reflect new functionality. - Adjusted the primary color in the dark theme for better visibility. - Enhanced the auth button height for improved usability.
This commit is contained in:
parent
6a4813c15e
commit
dd2bb57e42
|
@ -10,6 +10,7 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-ve
|
|||
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
|
||||
|
@ -25,7 +26,7 @@ class AppPages {
|
|||
// Auth
|
||||
GetPage(
|
||||
name: AppRoutes.roleSelection,
|
||||
page: () => const RoleSelectionScreen(),
|
||||
page: () => const RoleSignupPageView(),
|
||||
),
|
||||
|
||||
GetPage(
|
||||
|
|
|
@ -140,7 +140,7 @@ class AuthenticationRepository extends GetxController {
|
|||
if (isFirstTime) {
|
||||
_navigateToRoute(AppRoutes.onboarding);
|
||||
} else {
|
||||
_navigateToRoute(AppRoutes.onboarding);
|
||||
_navigateToRoute(AppRoutes.signIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:get/get.dart';
|
|||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||
|
@ -53,12 +54,6 @@ class SignupWithRoleController extends GetxController {
|
|||
// Check if Apple Sign In is available (only on iOS)
|
||||
final RxBool isAppleSignInAvailable = RxBool(Platform.isIOS);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
emailController.dispose();
|
||||
|
@ -67,55 +62,6 @@ class SignupWithRoleController extends GetxController {
|
|||
super.onClose();
|
||||
}
|
||||
|
||||
// Load available roles
|
||||
Future<void> loadRoles() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
final roles = await roleRepository.getAllRoles();
|
||||
availableRoles.assignAll(roles);
|
||||
|
||||
// Pre-select the default roles based on roleType
|
||||
_updateSelectedRoleBasedOnType();
|
||||
} catch (e) {
|
||||
Logger().e('Error loading roles: $e');
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message: 'Failed to load roles. Please try again.',
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Set role type (viewer or officer)
|
||||
void setRoleType(RoleType type) {
|
||||
roleType.value = type;
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
|
||||
// Update selected role based on roleType
|
||||
void _updateSelectedRoleBasedOnType() {
|
||||
if (availableRoles.isNotEmpty) {
|
||||
if (roleType.value == RoleType.officer) {
|
||||
// Find an officer role
|
||||
final officerRole = availableRoles.firstWhere(
|
||||
(role) => role.isOfficer,
|
||||
orElse: () => availableRoles.first,
|
||||
);
|
||||
selectedRoleId.value = officerRole.id;
|
||||
selectedRole.value = officerRole;
|
||||
} else {
|
||||
// Find a viewer role
|
||||
final viewerRole = availableRoles.firstWhere(
|
||||
(role) => !role.isOfficer,
|
||||
orElse: () => availableRoles.first,
|
||||
);
|
||||
selectedRoleId.value = viewerRole.id;
|
||||
selectedRole.value = viewerRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validators
|
||||
String? validateEmail(String? value) {
|
||||
final error = TValidators.validateEmail(value);
|
||||
|
@ -168,7 +114,7 @@ class SignupWithRoleController extends GetxController {
|
|||
|
||||
// Sign up function
|
||||
/// Updated signup function with better error handling and argument passing
|
||||
void signUp(bool isOfficer) async {
|
||||
void signUp() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
Logger().i('SignUp process started');
|
||||
|
@ -196,13 +142,16 @@ class SignupWithRoleController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Ensure we have a role selected
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
// Get selected role info before authentication
|
||||
|
||||
final roleController = RoleSelectionController.instance;
|
||||
final isOfficer = roleController.isOfficer.value;
|
||||
final roleId = roleController.selectedRole.value!.id;
|
||||
|
||||
Logger().i('Is officer: $isOfficer');
|
||||
|
||||
// Validate role selection
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
if (roleId.isEmpty) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Role Required',
|
||||
message: 'Please select a role before continuing.',
|
||||
|
@ -213,11 +162,13 @@ class SignupWithRoleController extends GetxController {
|
|||
// Create comprehensive initial user metadata
|
||||
final initialMetadata = UserMetadataModel(
|
||||
email: emailController.text.trim(),
|
||||
roleId: selectedRoleId.value,
|
||||
roleId: roleId,
|
||||
isOfficer: isOfficer,
|
||||
profileStatus: 'incomplete',
|
||||
);
|
||||
|
||||
Logger().i('Initial metadata: ${initialMetadata.toJson()}');
|
||||
|
||||
// Create the account
|
||||
final authResponse = await AuthenticationRepository.instance
|
||||
.initialSignUp(
|
||||
|
@ -238,11 +189,6 @@ class SignupWithRoleController extends GetxController {
|
|||
final user = authResponse.user!;
|
||||
Logger().d('Account created successfully for user: ${user.id}');
|
||||
|
||||
// Store temporary data for verification process
|
||||
await _storeTemporaryData(authResponse, isOfficer);
|
||||
|
||||
// Navigate with arguments
|
||||
Logger().i('Navigating to registration form');
|
||||
AuthenticationRepository.instance.screenRedirect();
|
||||
} catch (e) {
|
||||
Logger().e('Error during signup: $e');
|
||||
|
@ -264,24 +210,6 @@ class SignupWithRoleController extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
/// Store temporary data for the verification process
|
||||
Future<void> _storeTemporaryData(
|
||||
AuthResponse authResponse,
|
||||
bool isOfficer,
|
||||
) async {
|
||||
try {
|
||||
await storage.write('CURRENT_USER_EMAIL', emailController.text.trim());
|
||||
await storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken);
|
||||
await storage.write('TEMP_USER_ID', authResponse.user?.id);
|
||||
await storage.write('TEMP_ROLE_ID', selectedRoleId.value);
|
||||
await storage.write('TEMP_IS_OFFICER', isOfficer);
|
||||
|
||||
Logger().d('Temporary data stored successfully');
|
||||
} catch (e) {
|
||||
Logger().e('Failed to store temporary data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Sign in with Google
|
||||
Future<void> signInWithGoogle() async {
|
||||
try {
|
||||
|
@ -297,14 +225,10 @@ class SignupWithRoleController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get selected role info before authentication
|
||||
final roleType = this.roleType.value;
|
||||
final isOfficer = roleType == RoleType.officer;
|
||||
|
||||
// Make sure we have a role selected
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
// Get selected role from local storage
|
||||
final roleController = RoleSelectionController.instance;
|
||||
final isOfficer = roleController.isOfficer.value;
|
||||
final roleId = roleController.selectedRole.value!.id;
|
||||
|
||||
// Authenticate with Google
|
||||
final authResponse =
|
||||
|
@ -320,7 +244,7 @@ class SignupWithRoleController extends GetxController {
|
|||
// Create or update user metadata with role information
|
||||
final userMetadata = UserMetadataModel(
|
||||
isOfficer: isOfficer,
|
||||
roleId: selectedRoleId.value,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
// Update user metadata in the database
|
||||
|
@ -372,14 +296,9 @@ class SignupWithRoleController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get selected role info before authentication
|
||||
final roleType = this.roleType.value;
|
||||
final isOfficer = roleType == RoleType.officer;
|
||||
|
||||
// Make sure we have a role selected
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
final roleController = RoleSelectionController.instance;
|
||||
final isOfficer = roleController.isOfficer.value;
|
||||
final roleId = roleController.selectedRole.value!.id;
|
||||
|
||||
// Authenticate with Apple
|
||||
final authResponse =
|
||||
|
@ -397,7 +316,7 @@ class SignupWithRoleController extends GetxController {
|
|||
// Create or update user metadata with role information
|
||||
final userMetadata = UserMetadataModel(
|
||||
isOfficer: isOfficer,
|
||||
roleId: selectedRoleId.value,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
// Update user metadata in the database
|
||||
|
@ -442,14 +361,10 @@ class SignupWithRoleController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get selected role info before authentication
|
||||
final roleType = this.roleType.value;
|
||||
final isOfficer = roleType == RoleType.officer;
|
||||
|
||||
// Make sure we have a role selected
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
// Get selected role from local storage
|
||||
final roleController = RoleSelectionController.instance;
|
||||
final isOfficer = roleController.isOfficer.value;
|
||||
final roleId = roleController.selectedRole.value!.id;
|
||||
|
||||
// Authenticate with Facebook
|
||||
final authResponse =
|
||||
|
@ -467,7 +382,7 @@ class SignupWithRoleController extends GetxController {
|
|||
// Create or update user metadata with role information
|
||||
final userMetadata = UserMetadataModel(
|
||||
isOfficer: isOfficer,
|
||||
roleId: selectedRoleId.value,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
// Update user metadata in the database
|
||||
|
@ -512,14 +427,10 @@ class SignupWithRoleController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get selected role info before authentication
|
||||
final roleType = this.roleType.value;
|
||||
final isOfficer = roleType == RoleType.officer;
|
||||
|
||||
// Make sure we have a role selected
|
||||
if (selectedRoleId.value.isEmpty) {
|
||||
_updateSelectedRoleBasedOnType();
|
||||
}
|
||||
// Get selected role from local storage
|
||||
final roleController = RoleSelectionController.instance;
|
||||
final isOfficer = roleController.isOfficer.value;
|
||||
final roleId = roleController.selectedRole.value!.id;
|
||||
|
||||
// Authenticate with email and password
|
||||
final authResponse = await AuthenticationRepository.instance
|
||||
|
@ -540,7 +451,7 @@ class SignupWithRoleController extends GetxController {
|
|||
// Create or update user metadata with role information
|
||||
final userMetadata = UserMetadataModel(
|
||||
isOfficer: isOfficer,
|
||||
roleId: selectedRoleId.value,
|
||||
roleId: roleId,
|
||||
);
|
||||
|
||||
// Update user metadata in the database
|
||||
|
|
|
@ -13,10 +13,32 @@ final RxBool isFormValid = RxBool(true);
|
|||
// Controllers
|
||||
final nrpController = TextEditingController();
|
||||
final rankController = TextEditingController();
|
||||
final unitIdController = TextEditingController();
|
||||
final patrolUnitIdController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
final positionController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final validUntilController = TextEditingController();
|
||||
final avatarController = TextEditingController();
|
||||
final qrCodeController = TextEditingController();
|
||||
final bannedReasonController = TextEditingController();
|
||||
final bannedUntilController = TextEditingController();
|
||||
|
||||
// Error states
|
||||
final RxString nrpError = ''.obs;
|
||||
final RxString rankError = ''.obs;
|
||||
final RxString unitIdError = ''.obs;
|
||||
final RxString patrolUnitIdError = ''.obs;
|
||||
final RxString nameError = ''.obs;
|
||||
final RxString positionError = ''.obs;
|
||||
final RxString phoneError = ''.obs;
|
||||
final RxString emailError = ''.obs;
|
||||
final RxString validUntilError = ''.obs;
|
||||
final RxString avatarError = ''.obs;
|
||||
final RxString qrCodeError = ''.obs;
|
||||
final RxString bannedReasonError = ''.obs;
|
||||
final RxString bannedUntilError = ''.obs;
|
||||
|
||||
bool validate(GlobalKey<FormState> formKey) {
|
||||
clearErrors();
|
||||
|
@ -47,18 +69,150 @@ final RxBool isFormValid = RxBool(true);
|
|||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final unitIdValidation = TValidators.validateUserInput(
|
||||
'Unit ID',
|
||||
unitIdController.text,
|
||||
50,
|
||||
);
|
||||
if (unitIdValidation != null) {
|
||||
unitIdError.value = unitIdValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final patrolUnitIdValidation = TValidators.validateUserInput(
|
||||
'Patrol Unit ID',
|
||||
patrolUnitIdController.text,
|
||||
50,
|
||||
);
|
||||
if (patrolUnitIdValidation != null) {
|
||||
patrolUnitIdError.value = patrolUnitIdValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final nameValidation = TValidators.validateUserInput(
|
||||
'Name',
|
||||
nameController.text,
|
||||
50,
|
||||
);
|
||||
if (nameValidation != null) {
|
||||
nameError.value = nameValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final positionValidation = TValidators.validateUserInput(
|
||||
'Position',
|
||||
positionController.text,
|
||||
50,
|
||||
);
|
||||
if (positionValidation != null) {
|
||||
positionError.value = positionValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final phoneValidation = TValidators.validateUserInput(
|
||||
'Phone',
|
||||
phoneController.text,
|
||||
50,
|
||||
);
|
||||
if (phoneValidation != null) {
|
||||
phoneError.value = phoneValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final emailValidation = TValidators.validateUserInput(
|
||||
'Email',
|
||||
emailController.text,
|
||||
50,
|
||||
);
|
||||
if (emailValidation != null) {
|
||||
emailError.value = emailValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final validUntilValidation = TValidators.validateUserInput(
|
||||
'Valid Until',
|
||||
validUntilController.text,
|
||||
50,
|
||||
);
|
||||
if (validUntilValidation != null) {
|
||||
validUntilError.value = validUntilValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final avatarValidation = TValidators.validateUserInput(
|
||||
'Avatar',
|
||||
avatarController.text,
|
||||
50,
|
||||
);
|
||||
if (avatarValidation != null) {
|
||||
avatarError.value = avatarValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final qrCodeValidation = TValidators.validateUserInput(
|
||||
'QR Code',
|
||||
qrCodeController.text,
|
||||
50,
|
||||
);
|
||||
if (qrCodeValidation != null) {
|
||||
qrCodeError.value = qrCodeValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final bannedReasonValidation = TValidators.validateUserInput(
|
||||
'Banned Reason',
|
||||
bannedReasonController.text,
|
||||
50,
|
||||
);
|
||||
if (bannedReasonValidation != null) {
|
||||
bannedReasonError.value = bannedReasonValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
final bannedUntilValidation = TValidators.validateUserInput(
|
||||
'Banned Until',
|
||||
bannedUntilController.text,
|
||||
50,
|
||||
);
|
||||
if (bannedUntilValidation != null) {
|
||||
bannedUntilError.value = bannedUntilValidation;
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
return isFormValid.value;
|
||||
}
|
||||
|
||||
void clearErrors() {
|
||||
nrpError.value = '';
|
||||
rankError.value = '';
|
||||
unitIdError.value = '';
|
||||
patrolUnitIdError.value = '';
|
||||
nameError.value = '';
|
||||
positionError.value = '';
|
||||
phoneError.value = '';
|
||||
emailError.value = '';
|
||||
validUntilError.value = '';
|
||||
avatarError.value = '';
|
||||
qrCodeError.value = '';
|
||||
bannedReasonError.value = '';
|
||||
bannedUntilError.value = '';
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
nrpController.dispose();
|
||||
rankController.dispose();
|
||||
unitIdController.dispose();
|
||||
patrolUnitIdController.dispose();
|
||||
nameController.dispose();
|
||||
positionController.dispose();
|
||||
phoneController.dispose();
|
||||
emailController.dispose();
|
||||
validUntilController.dispose();
|
||||
avatarController.dispose();
|
||||
qrCodeController.dispose();
|
||||
bannedReasonController.dispose();
|
||||
bannedUntilController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
AppBar _buildAppBar(BuildContext context, bool dark) {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Text(
|
||||
|
@ -89,15 +90,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: dark ? TColors.white : TColors.black,
|
||||
size: TSizes.iconMd,
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
centerTitle: true,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
|||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.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/silver-app-bar/custom_silverbar.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||
|
@ -19,52 +19,34 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<SignupWithRoleController>();
|
||||
final theme = Theme.of(context);
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Scaffold(
|
||||
body: Obx(
|
||||
() => NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
// Top image section as SliverAppBar
|
||||
_buildSliverAppBar(controller, context),
|
||||
|
||||
// Tab bar as pinned SliverPersistentHeader
|
||||
SliverPersistentHeader(
|
||||
delegate: TSliverTabBarDelegate(
|
||||
child: _buildTabBar(context, controller),
|
||||
minHeight: 70, // Height including padding
|
||||
maxHeight: 70, // Fixed height for the tab bar
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
];
|
||||
},
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? TColors.dark : TColors.white,
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
_buildSignupForm(context, controller),
|
||||
]),
|
||||
),
|
||||
),
|
||||
// Add extra padding at the bottom for safe area
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Logo di dalam container form
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: TSizes.lg),
|
||||
child: Hero(
|
||||
tag: 'app_logo',
|
||||
child: SvgPicture.asset(
|
||||
isDark ? TImages.darkAppBgLogo : TImages.lightAppBgLogo,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSignupForm(context, controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -74,258 +56,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
SliverAppBar _buildSliverAppBar(
|
||||
SignupWithRoleController controller,
|
||||
BuildContext context,
|
||||
) {
|
||||
bool isOfficer = controller.roleType.value == RoleType.officer;
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
return SliverAppBar(
|
||||
expandedHeight: MediaQuery.of(context).size.height * 0.35,
|
||||
pinned: true,
|
||||
backgroundColor: isDark ? TColors.dark : TColors.primary,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Stack(
|
||||
children: [
|
||||
// Background gradient with rounded bottom corners
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
isDark ? Colors.black : TColors.primary,
|
||||
isDark ? TColors.dark : TColors.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(30),
|
||||
bottomRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Role image
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Responsive image size based on available height/width
|
||||
final double maxImageHeight = constraints.maxHeight * 0.9;
|
||||
final double maxImageWidth = constraints.maxWidth * 0.9;
|
||||
final double imageSize =
|
||||
maxImageHeight < maxImageWidth
|
||||
? maxImageHeight
|
||||
: maxImageWidth;
|
||||
|
||||
return SizedBox(
|
||||
height: imageSize,
|
||||
width: imageSize,
|
||||
child: SvgPicture.asset(
|
||||
isOfficer
|
||||
? (isDark
|
||||
? TImages.communicationDark
|
||||
: TImages.communication)
|
||||
: (isDark ? TImages.fallingDark : TImages.falling),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Back button with rounded container
|
||||
leading: Padding(
|
||||
padding: EdgeInsets.only(top: topPadding * 0.2),
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(left: TSizes.md),
|
||||
padding: const EdgeInsets.all(TSizes.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||
),
|
||||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Add rounded action button in top right corner
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: topPadding * 0.2, right: TSizes.md),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(TSizes.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.help_outline, color: Colors.white),
|
||||
onPressed: () {
|
||||
// Show help information
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('Account Types'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Viewer: Regular user account for general app access',
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Officer: Security personnel account with additional features and permissions',
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('Got it'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabBar(
|
||||
BuildContext context,
|
||||
SignupWithRoleController controller,
|
||||
) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
TSizes.defaultSpace,
|
||||
TSizes.xs,
|
||||
TSizes.defaultSpace,
|
||||
TSizes.xs,
|
||||
),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Viewer Tab
|
||||
_buildTab(
|
||||
context: context,
|
||||
controller: controller,
|
||||
roleType: RoleType.viewer,
|
||||
label: 'Viewer',
|
||||
icon: Icons.person,
|
||||
),
|
||||
|
||||
// Officer Tab
|
||||
_buildTab(
|
||||
context: context,
|
||||
controller: controller,
|
||||
roleType: RoleType.officer,
|
||||
label: 'Officer',
|
||||
icon: Icons.security,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab({
|
||||
required BuildContext context,
|
||||
required SignupWithRoleController controller,
|
||||
required RoleType roleType,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
bool isSelected = controller.roleType.value == roleType;
|
||||
Color selectedColor =
|
||||
roleType == RoleType.viewer ? TColors.primary : TColors.primary;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.setRoleType(roleType),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? selectedColor : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
||||
),
|
||||
// Add padding to make content larger
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
// Increase icon size from 18 to 22 or 24
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10), // Increased from 8
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
// Increase text size
|
||||
fontSize: 16, // Add explicit font size
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSignupForm(
|
||||
BuildContext context,
|
||||
SignupWithRoleController controller,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
bool isOfficer = controller.roleType.value == RoleType.officer;
|
||||
Color themeColor = isOfficer ? TColors.primary : TColors.primary;
|
||||
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Form(
|
||||
|
@ -336,15 +72,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
Text(
|
||||
'Create Your Account',
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeColor,
|
||||
color: isDark ? TColors.light : TColors.dark,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
isOfficer
|
||||
? 'Sign up as a security officer to access all features'
|
||||
: 'Sign up as a viewer to explore the application',
|
||||
'Please fill in the details below to create your account',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: isDark ? TColors.textSecondary : Colors.grey.shade600,
|
||||
),
|
||||
|
@ -352,7 +85,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
const SizedBox(height: TSizes.spaceBtwSections),
|
||||
|
||||
// Social Login Buttons
|
||||
_buildSocialLoginButtons(controller, themeColor, isDark),
|
||||
_buildSocialLoginButtons(controller, isDark),
|
||||
|
||||
// Or divider
|
||||
const AuthDivider(text: 'OR'),
|
||||
|
@ -370,7 +103,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
textInputAction: TextInputAction.next,
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
hintText: 'Enter your email',
|
||||
accentColor: themeColor,
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -386,7 +118,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
textInputAction: TextInputAction.next,
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
hintText: 'Enter your password',
|
||||
accentColor: themeColor,
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -402,7 +133,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
textInputAction: TextInputAction.done,
|
||||
prefixIcon: const Icon(Icons.lock_outlined),
|
||||
hintText: 'Confirm your password',
|
||||
accentColor: themeColor,
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -413,9 +143,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
fillColor: WidgetStateProperty.resolveWith<Color>((
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return themeColor;
|
||||
}
|
||||
return isDark ? Colors.grey.shade700 : Colors.grey.shade300;
|
||||
}),
|
||||
),
|
||||
|
@ -448,13 +175,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
Obx(
|
||||
() => AuthButton(
|
||||
text: 'Sign Up',
|
||||
onPressed: () => controller.signUp(isOfficer),
|
||||
onPressed: () => controller.signUp(),
|
||||
isLoading: controller.isLoading.value,
|
||||
backgroundColor: themeColor,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
|
||||
|
||||
// Already have an account row
|
||||
Row(
|
||||
|
@ -468,11 +194,21 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
),
|
||||
TextButton(
|
||||
onPressed: controller.goToSignIn,
|
||||
style: TextButton.styleFrom(
|
||||
overlayColor: TColors.transparent,
|
||||
foregroundColor:
|
||||
isDark
|
||||
? TColors.light.withOpacity(0.8)
|
||||
: TColors.primary.withOpacity(0.8),
|
||||
),
|
||||
child: Text(
|
||||
'Sign In',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: themeColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color:
|
||||
isDark
|
||||
? TColors.light.withOpacity(0.8)
|
||||
: TColors.primary.withOpacity(0.8),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -488,7 +224,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
|||
|
||||
Widget _buildSocialLoginButtons(
|
||||
SignupWithRoleController controller,
|
||||
Color themeColor,
|
||||
|
||||
bool isDark,
|
||||
) {
|
||||
return Column(
|
||||
|
|
|
@ -29,6 +29,44 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// Unit ID field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Unit ID',
|
||||
controller: controller.unitIdController,
|
||||
validator: (v) => TValidators.validateUserInput('Unit ID', v, 20),
|
||||
errorText: controller.unitIdError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., POLRES01',
|
||||
onChanged: (value) {
|
||||
controller.unitIdController.text = value;
|
||||
controller.unitIdError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Patrol Unit ID field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Patrol Unit ID',
|
||||
controller: controller.patrolUnitIdController,
|
||||
validator:
|
||||
(v) => TValidators.validateUserInput(
|
||||
'Patrol Unit ID',
|
||||
v,
|
||||
100,
|
||||
required: true,
|
||||
),
|
||||
errorText: controller.patrolUnitIdError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., PATROL01',
|
||||
onChanged: (value) {
|
||||
controller.patrolUnitIdController.text = value;
|
||||
controller.patrolUnitIdError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// NRP field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
|
@ -53,7 +91,7 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
controller: controller.rankController,
|
||||
validator: TValidators.validateRank,
|
||||
errorText: controller.rankError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., Captain',
|
||||
onChanged: (value) {
|
||||
controller.rankController.text = value;
|
||||
|
@ -61,6 +99,28 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Position field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Position',
|
||||
controller: controller.positionController,
|
||||
validator:
|
||||
(v) => TValidators.validateUserInput(
|
||||
'Position',
|
||||
v,
|
||||
100,
|
||||
required: true,
|
||||
),
|
||||
errorText: controller.positionError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
hintText: 'e.g., Head of Unit',
|
||||
onChanged: (value) {
|
||||
controller.positionController.text = value;
|
||||
controller.positionError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ class AuthButton extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 50,
|
||||
height: 55,
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
||||
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
enum RoleType { viewer, officer }
|
||||
|
||||
class RoleSelectionController extends GetxController {
|
||||
static RoleSelectionController get instance => Get.find();
|
||||
|
||||
|
@ -16,6 +17,9 @@ class RoleSelectionController extends GetxController {
|
|||
// Selected role
|
||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||
|
||||
// Role type (Viewer or Officer)
|
||||
final Rx<RoleType> roleType = RoleType.viewer.obs;
|
||||
|
||||
// Loading state
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isOfficer = false.obs;
|
||||
|
@ -28,7 +32,6 @@ class RoleSelectionController extends GetxController {
|
|||
super.onInit();
|
||||
fetchRoles();
|
||||
}
|
||||
|
||||
|
||||
// Fetch available roles from repository
|
||||
Future<void> fetchRoles() async {
|
||||
|
@ -62,44 +65,53 @@ class RoleSelectionController extends GetxController {
|
|||
// Select a role
|
||||
void selectRole(RoleModel role) {
|
||||
selectedRole.value = role;
|
||||
|
||||
// Set role type based on selected role
|
||||
if (role.name.toLowerCase() == 'officer') {
|
||||
roleType.value = RoleType.officer;
|
||||
isOfficer.value = true;
|
||||
} else {
|
||||
roleType.value = RoleType.viewer;
|
||||
isOfficer.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with selected role
|
||||
Future<void> continueWithRole() async {
|
||||
if (selectedRole.value == null) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message: 'Please select a role to continue.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Future<void> continueWithRole() async {
|
||||
// if (selectedRole.value == null) {
|
||||
// TLoaders.errorSnackBar(
|
||||
// title: 'Error',
|
||||
// message: 'Please select a role to continue.',
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// try {
|
||||
// isLoading.value = true;
|
||||
|
||||
// Logger().i('Selected role: ${selectedRole.value?.name}');
|
||||
// // Logger().i('Selected role: ${selectedRole.value?.name}');
|
||||
|
||||
// Check if the selected role is officer
|
||||
if (selectedRole.value?.name.toLowerCase() == 'officer') {
|
||||
isOfficer.value = true;
|
||||
} else {
|
||||
isOfficer.value = false;
|
||||
}
|
||||
// // Check if the selected role is officer
|
||||
// if (selectedRole.value?.name.toLowerCase() == 'officer') {
|
||||
// isOfficer.value = true;
|
||||
// } else {
|
||||
// isOfficer.value = false;
|
||||
// }
|
||||
|
||||
// Navigate directly to step form with selected role
|
||||
Get.toNamed(
|
||||
AppRoutes.registrationForm,
|
||||
arguments: {'role': selectedRole.value},
|
||||
);
|
||||
} catch (e) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message:
|
||||
'An error occurred while selecting the role. Please try again.',
|
||||
);
|
||||
// Logger().e('Error in continueWithRole: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
// // Navigate directly to step form with selected role
|
||||
// Get.toNamed(
|
||||
// AppRoutes.registrationForm,
|
||||
// arguments: {'role': selectedRole.value},
|
||||
// );
|
||||
// } catch (e) {
|
||||
// TLoaders.errorSnackBar(
|
||||
// title: 'Error',
|
||||
// message:
|
||||
// 'An error occurred while selecting the role. Please try again.',
|
||||
// );
|
||||
// // Logger().e('Error in continueWithRole: $e');
|
||||
// } finally {
|
||||
// isLoading.value = false;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
|||
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
||||
|
||||
class RoleSelectionScreen extends StatelessWidget {
|
||||
const RoleSelectionScreen({super.key});
|
||||
final void Function(String roleId)? onRoleSelected;
|
||||
const RoleSelectionScreen({super.key, this.onRoleSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -103,61 +104,10 @@ class RoleSelectionScreen extends StatelessWidget {
|
|||
}),
|
||||
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),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
() =>
|
||||
controller.selectedRole.value != null
|
||||
? _SwipeRightHint(isDark: isDark)
|
||||
: const SizedBox(height: 48),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -486,3 +436,74 @@ class RoleSelectionScreen extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Tambahkan widget animasi swipe hint di bawah:
|
||||
class _SwipeRightHint extends StatefulWidget {
|
||||
final bool isDark;
|
||||
const _SwipeRightHint({required this.isDark});
|
||||
|
||||
@override
|
||||
State<_SwipeRightHint> createState() => _SwipeRightHintState();
|
||||
}
|
||||
|
||||
class _SwipeRightHintState extends State<_SwipeRightHint>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
)..repeat(reverse: true);
|
||||
_animation = Tween<double>(
|
||||
begin: 0,
|
||||
end: 32,
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = widget.isDark ? Colors.white : const Color(0xFF2F2F2F);
|
||||
return SizedBox(
|
||||
height: 48,
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder:
|
||||
(context, child) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Swipe right to register',
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeMd,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Transform.translate(
|
||||
offset: Offset(_animation.value, 0),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
color: color,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class RoleSignupPageView extends StatefulWidget {
|
||||
const RoleSignupPageView({super.key});
|
||||
|
||||
@override
|
||||
State<RoleSignupPageView> createState() => _RoleSignupPageViewState();
|
||||
}
|
||||
|
||||
class _RoleSignupPageViewState extends State<RoleSignupPageView> {
|
||||
final PageController _pageController = PageController();
|
||||
int _currentPage = 0;
|
||||
String? _selectedRoleId;
|
||||
|
||||
void _onRoleSelected(String roleId) {
|
||||
setState(() {
|
||||
_selectedRoleId = roleId;
|
||||
_currentPage = 1;
|
||||
});
|
||||
_pageController.animateToPage(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
PageView(
|
||||
controller: _pageController,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
_currentPage = index;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
RoleSelectionScreen(onRoleSelected: _onRoleSelected),
|
||||
SignupWithRoleScreen(),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
2,
|
||||
(index) => AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
width: _currentPage == index ? 12 : 8,
|
||||
height: _currentPage == index ? 12 : 8,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_currentPage == index
|
||||
? TColors.primary
|
||||
: Colors.grey[400],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ class WelcomeScreen extends StatelessWidget {
|
|||
child: ElevatedButton(
|
||||
onPressed: controller.getStarted,
|
||||
style: theme.elevatedButtonTheme.style,
|
||||
child: Text('Get Started'),
|
||||
child: Text('Perform Location Check'),
|
||||
),
|
||||
),
|
||||
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
||||
|
|
|
@ -35,7 +35,7 @@ class TAppTheme {
|
|||
fontFamily: 'Poppins',
|
||||
disabledColor: TColors.grey,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: TColors.primary,
|
||||
primaryColor: TColors.light,
|
||||
textTheme: TTextTheme.darkTextTheme,
|
||||
chipTheme: TChipTheme.darkChipTheme,
|
||||
scaffoldBackgroundColor: TColors.black,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../constants/colors.dart';
|
||||
|
||||
/// Custom Class for Light & Dark Text Themes
|
||||
|
|
Loading…
Reference in New Issue