Refactor step indicator implementation and enhance registration form
- Removed the old StepIndicator widget and replaced it with a new implementation that supports multiple styles (standard, rounded, numbered). - Added new styles for the StepIndicator: NumberedStepIndicator, RoundedStepIndicator, and StandardStepIndicator. - Updated the registration form screen to utilize the new StepIndicator with improved styling and functionality. - Enhanced the StateScreen widget to adapt to dark mode and utilize constants for spacing and sizes. - Refactored CustomTextField to support dark mode and improved styling. - Introduced a new FormRegistrationScreen for handling user registration with multiple steps and validation.
This commit is contained in:
parent
60fb38da76
commit
ba4cbd180a
|
@ -9,15 +9,14 @@ class ServiceBindings extends Bindings {
|
|||
Future<void> dependencies() async {
|
||||
|
||||
// Initialize background service
|
||||
final supabaseService = await BackgroundService.instance
|
||||
.compute<SupabaseService, void>((message) => SupabaseService(), null);
|
||||
|
||||
final locationService = await BackgroundService.instance
|
||||
.compute<LocationService, void>((message) => LocationService(), null);
|
||||
final biometricService = await BackgroundService.instance
|
||||
.compute<BiometricService, void>((message) => BiometricService(), null);
|
||||
|
||||
// Initialize services
|
||||
await Get.putAsync(() => supabaseService.init(), permanent: true);
|
||||
await Get.putAsync(() => SupabaseService().init(), permanent: true);
|
||||
await Get.putAsync(() => biometricService.init(), permanent: true);
|
||||
await Get.putAsync(() => locationService.init(), permanent: true);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'package:get/get.dart';
|
|||
import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_screen.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/pages/step-form/step_form_screen.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_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/welcome/welcome_screen.dart';
|
||||
|
|
|
@ -5,9 +5,11 @@ import 'package:sigap/src/features/auth/presentasion/controllers/step_form_contr
|
|||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||
import 'package:sigap/src/shared/widgets/dropdown/custom_dropdown.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator/step_indicator.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/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class FormRegistrationScreen extends StatelessWidget {
|
||||
|
@ -17,6 +19,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
// Get the controller
|
||||
final controller = Get.find<FormRegistrationController>();
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
// Set system overlay style
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
|
@ -27,22 +30,25 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
backgroundColor: dark ? TColors.dark : TColors.light,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
||||
style: TextStyle(
|
||||
color: TColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
||||
icon: Icon(
|
||||
Icons.arrow_back,
|
||||
color: dark ? TColors.white : TColors.black,
|
||||
size: TSizes.iconMd,
|
||||
),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
|
@ -57,13 +63,14 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
children: [
|
||||
// Step indicator
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Obx(
|
||||
() => StepIndicator(
|
||||
currentStep: controller.currentStep.value,
|
||||
totalSteps: controller.stepFormKeys.length,
|
||||
stepTitles: _getStepTitles(controller.selectedRole.value!),
|
||||
onStepTapped: controller.goToStep,
|
||||
style: StepIndicatorStyle.standard,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -72,7 +79,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Obx(() {
|
||||
return _buildStepContent(controller);
|
||||
}),
|
||||
|
@ -82,7 +89,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
|
||||
// Navigation buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Row(
|
||||
children: [
|
||||
// Back button
|
||||
|
@ -91,7 +98,9 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
controller.currentStep.value > 0
|
||||
? Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
padding: const EdgeInsets.only(
|
||||
right: TSizes.sm,
|
||||
),
|
||||
child: AuthButton(
|
||||
text: 'Previous',
|
||||
onPressed: controller.previousStep,
|
||||
|
@ -106,7 +115,10 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: controller.currentStep.value > 0 ? 8.0 : 0.0,
|
||||
left:
|
||||
controller.currentStep.value > 0
|
||||
? TSizes.sm
|
||||
: 0.0,
|
||||
),
|
||||
child: Obx(
|
||||
() => AuthButton(
|
||||
|
@ -135,7 +147,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
if (role.isOfficer) {
|
||||
return ['Personal', 'Officer Info', 'Unit Info'];
|
||||
} else {
|
||||
return ['Personal', 'Emergency'];
|
||||
return ['Personal', 'Identity'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,7 +160,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
case 1:
|
||||
return isOfficer
|
||||
? _buildOfficerInfoStep(controller)
|
||||
: _buildEmergencyContactStep(controller);
|
||||
: _buildPrivacyIdentity(controller);
|
||||
case 2:
|
||||
// This step only exists for officers
|
||||
if (isOfficer) {
|
||||
|
@ -169,17 +181,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Text(
|
||||
'Personal Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: TSizes.fontSizeLg,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Please provide your personal details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeSm,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// First Name field
|
||||
Obx(
|
||||
|
@ -191,6 +206,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
TValidators.validateUserInput('First name', value, 50),
|
||||
errorText: controller.firstNameError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., John',
|
||||
onChanged: (value) {
|
||||
controller.firstNameController.text = value;
|
||||
controller.firstNameError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -208,6 +228,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
),
|
||||
errorText: controller.lastNameError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., Doe',
|
||||
onChanged: (value) {
|
||||
controller.lastNameController.text = value;
|
||||
controller.lastNameError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -220,6 +245,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
errorText: controller.phoneError.value,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., 081234567890',
|
||||
onChanged: (value) {
|
||||
controller.phoneController.text = value;
|
||||
controller.phoneError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -234,6 +264,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
errorText: controller.addressError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
maxLines: 3,
|
||||
hintText: 'e.g., 123 Main St, City, Country',
|
||||
onChanged: (value) {
|
||||
controller.addressController.text = value;
|
||||
controller.addressError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -241,7 +276,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildEmergencyContactStep(FormRegistrationController controller) {
|
||||
Widget _buildPrivacyIdentity(FormRegistrationController controller) {
|
||||
return Form(
|
||||
key: controller.stepFormKeys[1],
|
||||
child: Column(
|
||||
|
@ -250,17 +285,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Text(
|
||||
'Additional Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: TSizes.fontSizeLg,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Please provide additional personal details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeSm,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// NIK field
|
||||
Obx(
|
||||
|
@ -272,6 +310,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
errorText: controller.nikError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: 'e.g., 1234567890123456',
|
||||
onChanged: (value) {
|
||||
controller.nikController.text = value;
|
||||
controller.nikError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -291,6 +334,10 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
textInputAction: TextInputAction.next,
|
||||
maxLines: 3,
|
||||
hintText: 'Tell us a little about yourself (optional)',
|
||||
onChanged: (value) {
|
||||
controller.bioController.text = value;
|
||||
controller.bioError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -306,6 +353,10 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.datetime,
|
||||
hintText: 'e.g., 1990-01-31',
|
||||
onChanged: (value) {
|
||||
controller.birthDateController.text = value;
|
||||
controller.birthDateError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -322,17 +373,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Text(
|
||||
'Officer Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: TSizes.fontSizeLg,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Please provide your officer details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeSm,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// NRP field
|
||||
Obx(
|
||||
|
@ -342,6 +396,12 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
validator: TValidators.validateNRP,
|
||||
errorText: controller.nrpError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: 'e.g., 123456789',
|
||||
onChanged: (value) {
|
||||
controller.nrpController.text = value;
|
||||
controller.nrpError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -353,6 +413,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
validator: TValidators.validateRank,
|
||||
errorText: controller.rankError.value,
|
||||
textInputAction: TextInputAction.done,
|
||||
hintText: 'e.g., Captain',
|
||||
onChanged: (value) {
|
||||
controller.rankController.text = value;
|
||||
controller.rankError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -371,17 +436,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
Text(
|
||||
'Unit Information',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: TSizes.fontSizeLg,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Please provide your unit details',
|
||||
style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
||||
style: TextStyle(
|
||||
fontSize: TSizes.fontSizeSm,
|
||||
color: TColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// Position field
|
||||
Obx(
|
||||
|
@ -391,6 +459,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
|||
validator: TValidators.validatePosition,
|
||||
errorText: controller.positionError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., Commander',
|
||||
onChanged: (value) {
|
||||
controller.positionController.text = value;
|
||||
controller.positionError.value = '';
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class AuthButton extends StatelessWidget {
|
||||
final String text;
|
||||
|
@ -20,7 +20,7 @@ class AuthButton extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
height: TSizes.buttonHeight * 3, // Using consistent button height
|
||||
child:
|
||||
isPrimary
|
||||
? ElevatedButton(
|
||||
|
@ -29,16 +29,16 @@ class AuthButton extends StatelessWidget {
|
|||
backgroundColor: TColors.primary,
|
||||
foregroundColor: TColors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
elevation: 0,
|
||||
elevation: TSizes.buttonElevation,
|
||||
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: TSizes.iconMd,
|
||||
height: TSizes.iconMd,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
|
@ -49,7 +49,7 @@ class AuthButton extends StatelessWidget {
|
|||
: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: TSizes.fontSizeMd,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
@ -60,15 +60,15 @@ class AuthButton extends StatelessWidget {
|
|||
foregroundColor: TColors.primary,
|
||||
side: BorderSide(color: TColors.primary),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
disabledForegroundColor: TColors.primary.withOpacity(0.6),
|
||||
),
|
||||
child:
|
||||
isLoading
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: TSizes.iconMd,
|
||||
height: TSizes.iconMd,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
|
@ -79,7 +79,7 @@ class AuthButton extends StatelessWidget {
|
|||
: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: TSizes.fontSizeMd,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class AuthDivider extends StatelessWidget {
|
||||
final String text;
|
||||
|
@ -10,15 +11,27 @@ class AuthDivider extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: Divider(color: TColors.borderPrimary, thickness: 1)),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: TColors.borderPrimary,
|
||||
thickness: TSizes.dividerHeight,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(color: TColors.textSecondary, fontSize: 14),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: TColors.borderPrimary,
|
||||
thickness: TSizes.dividerHeight,
|
||||
),
|
||||
),
|
||||
Expanded(child: Divider(color: TColors.borderPrimary, thickness: 1)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class AuthHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
@ -8,6 +10,8 @@ class AuthHeader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -15,12 +19,12 @@ class AuthHeader extends StatelessWidget {
|
|||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: TSizes.spaceBtwSections),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/device/device_utility.dart';
|
||||
|
||||
class OtpInputField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
@ -18,9 +20,13 @@ class OtpInputField extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use responsive sizing based on screen width
|
||||
final width = TDeviceUtils.getScreenWidth(context) * 0.12;
|
||||
final height = width; // Square aspect ratio
|
||||
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: width,
|
||||
height: height,
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
|
@ -32,8 +38,7 @@ class OtpInputField extends StatelessWidget {
|
|||
LengthLimitingTextInputFormatter(1),
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
),
|
||||
|
@ -42,15 +47,15 @@ class OtpInputField extends StatelessWidget {
|
|||
filled: true,
|
||||
fillColor: TColors.lightContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||
borderSide: BorderSide(color: TColors.primary, width: 2),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
import '../../../../shared/widgets/text/custom_text_field.dart';
|
||||
|
||||
|
@ -38,6 +39,7 @@ class PasswordField extends StatelessWidget {
|
|||
icon: Icon(
|
||||
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
||||
color: TColors.textSecondary,
|
||||
size: TSizes.iconMd,
|
||||
),
|
||||
onPressed: onToggleVisibility,
|
||||
),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class SocialButton extends StatelessWidget {
|
||||
final String text;
|
||||
|
@ -15,17 +17,18 @@ class SocialButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
height: TSizes.buttonHeight * 3,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, color: TColors.textPrimary),
|
||||
icon: Icon(icon, color: TColors.textPrimary, size: TSizes.iconMd),
|
||||
label: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: TColors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
@ -33,7 +36,7 @@ class SocialButton extends StatelessWidget {
|
|||
foregroundColor: TColors.textPrimary,
|
||||
side: BorderSide(color: TColors.borderPrimary),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/device/device_utility.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
|
@ -11,7 +12,7 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
|||
this.controller,
|
||||
this.isScrollable = false,
|
||||
this.padding,
|
||||
this.indicatorColor = TColors.primary,
|
||||
this.indicatorColor,
|
||||
this.automaticIndicatorColorAdjustment = true,
|
||||
this.indicatorWeight = 2.0,
|
||||
this.indicatorPadding = EdgeInsets.zero,
|
||||
|
@ -19,10 +20,10 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
|||
this.indicatorSize,
|
||||
this.dividerColor,
|
||||
this.dividerHeight,
|
||||
this.labelColor = TColors.primary,
|
||||
this.labelColor,
|
||||
this.labelStyle,
|
||||
this.labelPadding,
|
||||
this.unselectedLabelColor = TColors.darkGrey,
|
||||
this.unselectedLabelColor,
|
||||
this.unselectedLabelStyle,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.overlayColor,
|
||||
|
@ -68,27 +69,35 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dark = THelperFunctions.isDarkMode(context);
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Material(
|
||||
color: dark ? TColors.black : TColors.white,
|
||||
color: isDark ? TColors.dark : TColors.white,
|
||||
child: TabBar(
|
||||
tabs: tabs,
|
||||
controller: controller,
|
||||
isScrollable: isScrollable,
|
||||
padding: padding,
|
||||
indicatorColor: indicatorColor,
|
||||
indicatorColor: indicatorColor ?? TColors.primary,
|
||||
automaticIndicatorColorAdjustment: automaticIndicatorColorAdjustment,
|
||||
indicatorWeight: indicatorWeight,
|
||||
indicatorPadding: indicatorPadding,
|
||||
indicator: indicator,
|
||||
indicatorSize: indicatorSize,
|
||||
dividerColor: dividerColor,
|
||||
dividerHeight: dividerHeight,
|
||||
labelColor: labelColor,
|
||||
labelStyle: labelStyle,
|
||||
labelPadding: labelPadding,
|
||||
unselectedLabelColor: unselectedLabelColor,
|
||||
unselectedLabelStyle: unselectedLabelStyle,
|
||||
dividerHeight: dividerHeight ?? TSizes.dividerHeight,
|
||||
labelColor: labelColor ?? TColors.primary,
|
||||
labelStyle:
|
||||
labelStyle ??
|
||||
Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
labelPadding:
|
||||
labelPadding ?? const EdgeInsets.symmetric(horizontal: TSizes.md),
|
||||
unselectedLabelColor:
|
||||
unselectedLabelColor ?? (isDark ? TColors.grey : TColors.darkGrey),
|
||||
unselectedLabelStyle:
|
||||
unselectedLabelStyle ?? Theme.of(context).textTheme.bodyMedium,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
enableFeedback: enableFeedback,
|
||||
|
@ -98,8 +107,19 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
|||
tabAlignment: tabAlignment,
|
||||
textScaler: textScaler,
|
||||
indicatorAnimation: indicatorAnimation,
|
||||
overlayColor: WidgetStateProperty.all(Colors.blue.shade50),
|
||||
splashBorderRadius: splashBorderRadius,
|
||||
overlayColor:
|
||||
overlayColor ??
|
||||
WidgetStateProperty.resolveWith<Color?>((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return TColors.primary.withOpacity(0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return TColors.primary.withOpacity(0.2);
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
splashBorderRadius:
|
||||
splashBorderRadius ?? BorderRadius.circular(TSizes.md),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class StepIndicator extends StatelessWidget {
|
||||
final int currentStep;
|
||||
final int totalSteps;
|
||||
final List<String> stepTitles;
|
||||
final Function(int) onStepTapped;
|
||||
|
||||
const StepIndicator({
|
||||
super.key,
|
||||
required this.currentStep,
|
||||
required this.totalSteps,
|
||||
required this.stepTitles,
|
||||
required this.onStepTapped,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
final isActive = index <= currentStep;
|
||||
final isLast = index == totalSteps - 1;
|
||||
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Step circle
|
||||
GestureDetector(
|
||||
onTap: () => onStepTapped(index),
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? TColors.primary : TColors.secondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child:
|
||||
isActive
|
||||
? Icon(
|
||||
index < currentStep
|
||||
? Icons.check
|
||||
: Icons.circle,
|
||||
color: TColors.white,
|
||||
size: index < currentStep ? 16 : 12,
|
||||
)
|
||||
: Text(
|
||||
'${index + 1}',
|
||||
style: TextStyle(
|
||||
color: TColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Connector line
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 2,
|
||||
color:
|
||||
index < currentStep
|
||||
? TColors.primary
|
||||
: TColors.borderPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return Expanded(
|
||||
child: Text(
|
||||
stepTitles[index],
|
||||
textAlign:
|
||||
index == 0
|
||||
? TextAlign.start
|
||||
: index == totalSteps - 1
|
||||
? TextAlign.end
|
||||
: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight:
|
||||
index == currentStep
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? TColors.primary
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export 'step_indicator.dart';
|
||||
export 'styles/numbered_step_indicator.dart';
|
||||
export 'styles/rounded_step_indicator.dart';
|
||||
export 'styles/standard_step_indicator.dart';
|
|
@ -0,0 +1,66 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator/styles/numbered_step_indicator.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator/styles/rounded_step_indicator.dart';
|
||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator/styles/standard_step_indicator.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
enum StepIndicatorStyle {
|
||||
standard, // Original style with improvements
|
||||
rounded, // Rounded connector lines
|
||||
numbered, // Numbered steps with different styling
|
||||
}
|
||||
|
||||
class StepIndicator extends StatelessWidget {
|
||||
final int currentStep;
|
||||
final int totalSteps;
|
||||
final List<String> stepTitles;
|
||||
final Function(int) onStepTapped;
|
||||
final StepIndicatorStyle style;
|
||||
|
||||
const StepIndicator({
|
||||
super.key,
|
||||
required this.currentStep,
|
||||
required this.totalSteps,
|
||||
required this.stepTitles,
|
||||
required this.onStepTapped,
|
||||
this.style = StepIndicatorStyle.standard,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
// Choose the appropriate style
|
||||
switch (style) {
|
||||
case StepIndicatorStyle.rounded:
|
||||
return RoundedStepIndicator(
|
||||
currentStep: currentStep,
|
||||
totalSteps: totalSteps,
|
||||
stepTitles: stepTitles,
|
||||
onStepTapped: onStepTapped,
|
||||
theme: theme,
|
||||
isDark: isDark,
|
||||
);
|
||||
case StepIndicatorStyle.numbered:
|
||||
return NumberedStepIndicator(
|
||||
currentStep: currentStep,
|
||||
totalSteps: totalSteps,
|
||||
stepTitles: stepTitles,
|
||||
onStepTapped: onStepTapped,
|
||||
theme: theme,
|
||||
isDark: isDark,
|
||||
);
|
||||
case StepIndicatorStyle.standard:
|
||||
default:
|
||||
return StandardStepIndicator(
|
||||
currentStep: currentStep,
|
||||
totalSteps: totalSteps,
|
||||
stepTitles: stepTitles,
|
||||
onStepTapped: onStepTapped,
|
||||
theme: theme,
|
||||
isDark: isDark,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class NumberedStepIndicator extends StatelessWidget {
|
||||
final int currentStep;
|
||||
final int totalSteps;
|
||||
final List<String> stepTitles;
|
||||
final Function(int) onStepTapped;
|
||||
final ThemeData theme;
|
||||
final bool isDark;
|
||||
|
||||
const NumberedStepIndicator({
|
||||
super.key,
|
||||
required this.currentStep,
|
||||
required this.totalSteps,
|
||||
required this.stepTitles,
|
||||
required this.onStepTapped,
|
||||
required this.theme,
|
||||
required this.isDark,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Special case for 2 steps
|
||||
if (totalSteps == 2) {
|
||||
return _buildTwoStepIndicator(context);
|
||||
}
|
||||
|
||||
// Normal case (3+ steps)
|
||||
return _buildMultiStepIndicator(context);
|
||||
}
|
||||
|
||||
Widget _buildTwoStepIndicator(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// First step
|
||||
_buildStepBox(0),
|
||||
|
||||
// Connector line
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: TSizes.dividerHeight,
|
||||
color:
|
||||
isDark ? TColors.darkerGrey : TColors.borderPrimary,
|
||||
),
|
||||
if (currentStep > 0)
|
||||
Container(
|
||||
height: TSizes.dividerHeight,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Second step
|
||||
_buildStepBox(1),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return Text(
|
||||
stepTitles[index],
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep ? FontWeight.bold : FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultiStepIndicator(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
final isLast = index == totalSteps - 1;
|
||||
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Step number
|
||||
_buildStepBox(index),
|
||||
|
||||
// Connector line
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: TSizes.dividerHeight,
|
||||
color:
|
||||
isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.borderPrimary,
|
||||
),
|
||||
Container(
|
||||
height: TSizes.dividerHeight,
|
||||
width:
|
||||
index < currentStep
|
||||
? MediaQuery.of(context).size.width
|
||||
: 0,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: index == 0 ? 0 : TSizes.xs,
|
||||
right: index == totalSteps - 1 ? 0 : TSizes.xs,
|
||||
),
|
||||
child: Text(
|
||||
stepTitles[index],
|
||||
textAlign:
|
||||
index == 0
|
||||
? TextAlign.start
|
||||
: index == totalSteps - 1
|
||||
? TextAlign.end
|
||||
: TextAlign.center,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepBox(int index) {
|
||||
final isActive = index <= currentStep;
|
||||
final isCompleted = index < currentStep;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onStepTapped(index),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: TSizes.xl + TSizes.xs,
|
||||
height: TSizes.xl + TSizes.xs,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCompleted
|
||||
? theme.primaryColor
|
||||
: isActive
|
||||
? theme.primaryColor.withOpacity(0.2)
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.secondary.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isActive ? theme.primaryColor : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child:
|
||||
isCompleted
|
||||
? Icon(Icons.check, color: Colors.white, size: TSizes.iconSm)
|
||||
: Text(
|
||||
'${index + 1}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
isActive
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white
|
||||
: TColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class RoundedStepIndicator extends StatelessWidget {
|
||||
final int currentStep;
|
||||
final int totalSteps;
|
||||
final List<String> stepTitles;
|
||||
final Function(int) onStepTapped;
|
||||
final ThemeData theme;
|
||||
final bool isDark;
|
||||
|
||||
const RoundedStepIndicator({
|
||||
super.key,
|
||||
required this.currentStep,
|
||||
required this.totalSteps,
|
||||
required this.stepTitles,
|
||||
required this.onStepTapped,
|
||||
required this.theme,
|
||||
required this.isDark,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Special case for 2 steps
|
||||
if (totalSteps == 2) {
|
||||
return _buildTwoStepIndicator(context);
|
||||
}
|
||||
|
||||
// Normal case (3+ steps)
|
||||
return _buildMultiStepIndicator(context);
|
||||
}
|
||||
|
||||
Widget _buildTwoStepIndicator(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// First step
|
||||
_buildStepCircle(0),
|
||||
|
||||
// Connector line
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: TSizes.dividerHeight,
|
||||
margin: EdgeInsets.symmetric(horizontal: TSizes.xs),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
currentStep > 0
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.borderPrimary,
|
||||
borderRadius: BorderRadius.circular(
|
||||
TSizes.dividerHeight / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Second step
|
||||
_buildStepCircle(1),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return SizedBox(
|
||||
width: TSizes.xl,
|
||||
child: Text(
|
||||
stepTitles[index],
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultiStepIndicator(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: TSizes.xl + TSizes.xs,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Connector lines first (in the background)
|
||||
Positioned.fill(
|
||||
top: TSizes.xl / 2 - TSizes.dividerHeight / 2,
|
||||
child: Row(
|
||||
children: List.generate(totalSteps - 1, (index) {
|
||||
final isActive = index < currentStep;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: TSizes.dividerHeight,
|
||||
margin: EdgeInsets.symmetric(horizontal: TSizes.xs),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isActive
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.borderPrimary,
|
||||
borderRadius: BorderRadius.circular(
|
||||
TSizes.dividerHeight / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
// Step circles on top
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return _buildStepCircle(index);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return SizedBox(
|
||||
width: TSizes.xl,
|
||||
child: Text(
|
||||
stepTitles[index],
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStepCircle(int index) {
|
||||
final isActive = index <= currentStep;
|
||||
return Container(
|
||||
width: TSizes.xl,
|
||||
height: TSizes.xl,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isActive
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.secondary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isActive ? theme.primaryColor : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
boxShadow:
|
||||
isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: theme.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onStepTapped(index),
|
||||
borderRadius: BorderRadius.circular(TSizes.xl),
|
||||
child: Center(
|
||||
child:
|
||||
isActive
|
||||
? Icon(
|
||||
index < currentStep ? Icons.check : Icons.circle,
|
||||
color: Colors.white,
|
||||
size: index < currentStep ? TSizes.iconSm : TSizes.iconXs,
|
||||
)
|
||||
: Text(
|
||||
'${index + 1}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: isDark ? Colors.white : TColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
|
||||
class StandardStepIndicator extends StatelessWidget {
|
||||
final int currentStep;
|
||||
final int totalSteps;
|
||||
final List<String> stepTitles;
|
||||
final Function(int) onStepTapped;
|
||||
final ThemeData theme;
|
||||
final bool isDark;
|
||||
|
||||
const StandardStepIndicator({
|
||||
super.key,
|
||||
required this.currentStep,
|
||||
required this.totalSteps,
|
||||
required this.stepTitles,
|
||||
required this.onStepTapped,
|
||||
required this.theme,
|
||||
required this.isDark,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Special case for 2 steps
|
||||
if (totalSteps == 2) {
|
||||
return _buildTwoStepIndicator(context);
|
||||
}
|
||||
|
||||
// Normal case (3+ steps)
|
||||
return _buildMultiStepIndicator(context);
|
||||
}
|
||||
|
||||
Widget _buildTwoStepIndicator(BuildContext context) {
|
||||
final circleWidth = TSizes.xl;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md / 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// First step
|
||||
Container(
|
||||
width: circleWidth,
|
||||
height: circleWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onStepTapped(0),
|
||||
borderRadius: BorderRadius.circular(circleWidth),
|
||||
child: Center(
|
||||
child:
|
||||
0 < currentStep
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
size: TSizes.iconSm,
|
||||
)
|
||||
: Icon(
|
||||
Icons.circle,
|
||||
color: Colors.white,
|
||||
size: TSizes.iconXs,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Connector line that spans the entire space between circles
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: TSizes.dividerHeight,
|
||||
color:
|
||||
currentStep > 0
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.borderPrimary,
|
||||
),
|
||||
),
|
||||
|
||||
// Second step
|
||||
Container(
|
||||
width: circleWidth,
|
||||
height: circleWidth,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
currentStep >= 1
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.secondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => onStepTapped(1),
|
||||
borderRadius: BorderRadius.circular(circleWidth),
|
||||
child: Center(
|
||||
child:
|
||||
currentStep >= 1
|
||||
? Icon(
|
||||
currentStep > 1 ? Icons.check : Icons.circle,
|
||||
color: Colors.white,
|
||||
size:
|
||||
currentStep > 1
|
||||
? TSizes.iconSm
|
||||
: TSizes.iconXs,
|
||||
)
|
||||
: Text(
|
||||
'2',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: TColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return Text(
|
||||
stepTitles[index],
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep ? FontWeight.bold : FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMultiStepIndicator(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
final isActive = index <= currentStep;
|
||||
final isLast = index == totalSteps - 1;
|
||||
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Step circle
|
||||
GestureDetector(
|
||||
onTap: () => onStepTapped(index),
|
||||
child: Container(
|
||||
width: TSizes.xl,
|
||||
height: TSizes.xl,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isActive
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.secondary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child:
|
||||
isActive
|
||||
? Icon(
|
||||
index < currentStep
|
||||
? Icons.check
|
||||
: Icons.circle,
|
||||
color: Colors.white,
|
||||
size:
|
||||
index < currentStep
|
||||
? TSizes.iconSm
|
||||
: TSizes.iconXs,
|
||||
)
|
||||
: Text(
|
||||
'${index + 1}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
isDark
|
||||
? Colors.white
|
||||
: TColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Connector line
|
||||
if (!isLast)
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: TSizes.dividerHeight,
|
||||
color:
|
||||
index < currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? TColors.darkerGrey
|
||||
: TColors.borderPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
SizedBox(height: TSizes.sm),
|
||||
|
||||
// Step titles
|
||||
Row(
|
||||
children: List.generate(totalSteps, (index) {
|
||||
return Expanded(
|
||||
child: Text(
|
||||
stepTitles[index],
|
||||
textAlign:
|
||||
index == 0
|
||||
? TextAlign.start
|
||||
: index == totalSteps - 1
|
||||
? TextAlign.end
|
||||
: TextAlign.center,
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
fontWeight:
|
||||
index == currentStep
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color:
|
||||
index == currentStep
|
||||
? theme.primaryColor
|
||||
: isDark
|
||||
? Colors.white70
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ import 'package:flutter/services.dart';
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/device/device_utility.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class StateScreen extends StatelessWidget {
|
||||
const StateScreen({super.key});
|
||||
|
@ -16,20 +19,22 @@ class StateScreen extends StatelessWidget {
|
|||
final message = args['message'] as String;
|
||||
final buttonText = args['buttonText'] as String;
|
||||
final onButtonPressed = args['onButtonPressed'] as Function();
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
// Set system overlay style
|
||||
TDeviceUtils.setStatusBarColor(Colors.transparent);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: TColors.light,
|
||||
backgroundColor: isDark ? TColors.dark : TColors.light,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
@ -39,33 +44,37 @@ class StateScreen extends StatelessWidget {
|
|||
type == 'success'
|
||||
? Icons.check_circle_outline
|
||||
: Icons.error_outline,
|
||||
size: 100,
|
||||
size: TSizes.imageThumbSize + TSizes.xl,
|
||||
color: type == 'success' ? TColors.success : TColors.error,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: TSizes.spaceBtwSections),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.textPrimary,
|
||||
color: isDark ? TColors.white : TColors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// Message
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(fontSize: 16, color: TColors.textSecondary),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color:
|
||||
isDark
|
||||
? TColors.white.withOpacity(0.7)
|
||||
: TColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
SizedBox(height: THelperFunctions.screenHeight() * 0.05),
|
||||
|
||||
// Button
|
||||
AuthButton(text: buttonText, onPressed: onButtonPressed),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
class CustomTextField extends StatelessWidget {
|
||||
final String label;
|
||||
|
@ -33,18 +35,19 @@ class CustomTextField extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: TColors.textPrimary,
|
||||
color: isDark ? TColors.white : TColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
|
@ -54,42 +57,46 @@ class CustomTextField extends StatelessWidget {
|
|||
textInputAction: textInputAction,
|
||||
maxLines: maxLines,
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(color: TColors.textPrimary, fontSize: 16),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: isDark ? TColors.white : TColors.textPrimary,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: TColors.textSecondary, fontSize: 16),
|
||||
hintStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||
errorText:
|
||||
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
horizontal: TSizes.md,
|
||||
vertical: TSizes.md,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
filled: true,
|
||||
fillColor: TColors.lightContainer,
|
||||
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.primary, width: 1.5),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.error, width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: TColors.error, width: 1.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue