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 {
|
Future<void> dependencies() async {
|
||||||
|
|
||||||
// Initialize background service
|
// Initialize background service
|
||||||
final supabaseService = await BackgroundService.instance
|
|
||||||
.compute<SupabaseService, void>((message) => SupabaseService(), null);
|
|
||||||
final locationService = await BackgroundService.instance
|
final locationService = await BackgroundService.instance
|
||||||
.compute<LocationService, void>((message) => LocationService(), null);
|
.compute<LocationService, void>((message) => LocationService(), null);
|
||||||
final biometricService = await BackgroundService.instance
|
final biometricService = await BackgroundService.instance
|
||||||
.compute<BiometricService, void>((message) => BiometricService(), null);
|
.compute<BiometricService, void>((message) => BiometricService(), null);
|
||||||
|
|
||||||
// Initialize services
|
// 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(() => biometricService.init(), permanent: true);
|
||||||
await Get.putAsync(() => locationService.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/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/signin/signin_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/signup_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/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_selection_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_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/auth/presentasion/widgets/auth_button.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/index.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/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/shared/widgets/text/custom_text_field.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class FormRegistrationScreen extends StatelessWidget {
|
class FormRegistrationScreen extends StatelessWidget {
|
||||||
|
@ -17,6 +19,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Get the controller
|
// Get the controller
|
||||||
final controller = Get.find<FormRegistrationController>();
|
final controller = Get.find<FormRegistrationController>();
|
||||||
|
final dark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Set system overlay style
|
// Set system overlay style
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
@ -27,22 +30,25 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: TColors.light,
|
backgroundColor: dark ? TColors.dark : TColors.light,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Obx(
|
title: Obx(
|
||||||
() => Text(
|
() => Text(
|
||||||
'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
||||||
style: TextStyle(
|
style: Theme.of(
|
||||||
color: TColors.textPrimary,
|
context,
|
||||||
fontWeight: FontWeight.bold,
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: IconButton(
|
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(),
|
onPressed: () => Get.back(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -57,13 +63,14 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
// Step indicator
|
// Step indicator
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => StepIndicator(
|
() => StepIndicator(
|
||||||
currentStep: controller.currentStep.value,
|
currentStep: controller.currentStep.value,
|
||||||
totalSteps: controller.stepFormKeys.length,
|
totalSteps: controller.stepFormKeys.length,
|
||||||
stepTitles: _getStepTitles(controller.selectedRole.value!),
|
stepTitles: _getStepTitles(controller.selectedRole.value!),
|
||||||
onStepTapped: controller.goToStep,
|
onStepTapped: controller.goToStep,
|
||||||
|
style: StepIndicatorStyle.standard,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -72,7 +79,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
return _buildStepContent(controller);
|
return _buildStepContent(controller);
|
||||||
}),
|
}),
|
||||||
|
@ -82,7 +89,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
|
|
||||||
// Navigation buttons
|
// Navigation buttons
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Back button
|
// Back button
|
||||||
|
@ -91,7 +98,9 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
controller.currentStep.value > 0
|
controller.currentStep.value > 0
|
||||||
? Expanded(
|
? Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(
|
||||||
|
right: TSizes.sm,
|
||||||
|
),
|
||||||
child: AuthButton(
|
child: AuthButton(
|
||||||
text: 'Previous',
|
text: 'Previous',
|
||||||
onPressed: controller.previousStep,
|
onPressed: controller.previousStep,
|
||||||
|
@ -106,7 +115,10 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: controller.currentStep.value > 0 ? 8.0 : 0.0,
|
left:
|
||||||
|
controller.currentStep.value > 0
|
||||||
|
? TSizes.sm
|
||||||
|
: 0.0,
|
||||||
),
|
),
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => AuthButton(
|
() => AuthButton(
|
||||||
|
@ -135,7 +147,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
if (role.isOfficer) {
|
if (role.isOfficer) {
|
||||||
return ['Personal', 'Officer Info', 'Unit Info'];
|
return ['Personal', 'Officer Info', 'Unit Info'];
|
||||||
} else {
|
} else {
|
||||||
return ['Personal', 'Emergency'];
|
return ['Personal', 'Identity'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +160,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
case 1:
|
case 1:
|
||||||
return isOfficer
|
return isOfficer
|
||||||
? _buildOfficerInfoStep(controller)
|
? _buildOfficerInfoStep(controller)
|
||||||
: _buildEmergencyContactStep(controller);
|
: _buildPrivacyIdentity(controller);
|
||||||
case 2:
|
case 2:
|
||||||
// This step only exists for officers
|
// This step only exists for officers
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
|
@ -169,17 +181,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'Personal Information',
|
'Personal Information',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: TSizes.fontSizeLg,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
'Please provide your personal details',
|
'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
|
// First Name field
|
||||||
Obx(
|
Obx(
|
||||||
|
@ -191,6 +206,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
TValidators.validateUserInput('First name', value, 50),
|
TValidators.validateUserInput('First name', value, 50),
|
||||||
errorText: controller.firstNameError.value,
|
errorText: controller.firstNameError.value,
|
||||||
textInputAction: TextInputAction.next,
|
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,
|
errorText: controller.lastNameError.value,
|
||||||
textInputAction: TextInputAction.next,
|
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,
|
errorText: controller.phoneError.value,
|
||||||
keyboardType: TextInputType.phone,
|
keyboardType: TextInputType.phone,
|
||||||
textInputAction: TextInputAction.next,
|
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,
|
errorText: controller.addressError.value,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
maxLines: 3,
|
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(
|
return Form(
|
||||||
key: controller.stepFormKeys[1],
|
key: controller.stepFormKeys[1],
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -250,17 +285,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'Additional Information',
|
'Additional Information',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: TSizes.fontSizeLg,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
'Please provide additional personal details',
|
'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
|
// NIK field
|
||||||
Obx(
|
Obx(
|
||||||
|
@ -272,6 +310,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
errorText: controller.nikError.value,
|
errorText: controller.nikError.value,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
keyboardType: TextInputType.number,
|
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,
|
textInputAction: TextInputAction.next,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
hintText: 'Tell us a little about yourself (optional)',
|
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,
|
textInputAction: TextInputAction.done,
|
||||||
keyboardType: TextInputType.datetime,
|
keyboardType: TextInputType.datetime,
|
||||||
hintText: 'e.g., 1990-01-31',
|
hintText: 'e.g., 1990-01-31',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.birthDateController.text = value;
|
||||||
|
controller.birthDateError.value = '';
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -322,17 +373,20 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'Officer Information',
|
'Officer Information',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: TSizes.fontSizeLg,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
'Please provide your officer details',
|
'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
|
// NRP field
|
||||||
Obx(
|
Obx(
|
||||||
|
@ -342,6 +396,12 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
validator: TValidators.validateNRP,
|
validator: TValidators.validateNRP,
|
||||||
errorText: controller.nrpError.value,
|
errorText: controller.nrpError.value,
|
||||||
textInputAction: TextInputAction.next,
|
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,
|
validator: TValidators.validateRank,
|
||||||
errorText: controller.rankError.value,
|
errorText: controller.rankError.value,
|
||||||
textInputAction: TextInputAction.done,
|
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(
|
Text(
|
||||||
'Unit Information',
|
'Unit Information',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: TSizes.fontSizeLg,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
'Please provide your unit details',
|
'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
|
// Position field
|
||||||
Obx(
|
Obx(
|
||||||
|
@ -391,6 +459,11 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
validator: TValidators.validatePosition,
|
validator: TValidators.validatePosition,
|
||||||
errorText: controller.positionError.value,
|
errorText: controller.positionError.value,
|
||||||
textInputAction: TextInputAction.next,
|
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:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
class AuthButton extends StatelessWidget {
|
class AuthButton extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
@ -20,7 +20,7 @@ class AuthButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: TSizes.buttonHeight * 3, // Using consistent button height
|
||||||
child:
|
child:
|
||||||
isPrimary
|
isPrimary
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
|
@ -29,16 +29,16 @@ class AuthButton extends StatelessWidget {
|
||||||
backgroundColor: TColors.primary,
|
backgroundColor: TColors.primary,
|
||||||
foregroundColor: TColors.white,
|
foregroundColor: TColors.white,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: TSizes.buttonElevation,
|
||||||
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
disabledBackgroundColor: TColors.primary.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
isLoading
|
isLoading
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
width: 24,
|
width: TSizes.iconMd,
|
||||||
height: 24,
|
height: TSizes.iconMd,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
@ -49,7 +49,7 @@ class AuthButton extends StatelessWidget {
|
||||||
: Text(
|
: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: TSizes.fontSizeMd,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -60,15 +60,15 @@ class AuthButton extends StatelessWidget {
|
||||||
foregroundColor: TColors.primary,
|
foregroundColor: TColors.primary,
|
||||||
side: BorderSide(color: TColors.primary),
|
side: BorderSide(color: TColors.primary),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
),
|
),
|
||||||
disabledForegroundColor: TColors.primary.withOpacity(0.6),
|
disabledForegroundColor: TColors.primary.withOpacity(0.6),
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
isLoading
|
isLoading
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
width: 24,
|
width: TSizes.iconMd,
|
||||||
height: 24,
|
height: TSizes.iconMd,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
@ -79,7 +79,7 @@ class AuthButton extends StatelessWidget {
|
||||||
: Text(
|
: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: TSizes.fontSizeMd,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
class AuthDivider extends StatelessWidget {
|
class AuthDivider extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
@ -10,15 +11,27 @@ class AuthDivider extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Divider(color: TColors.borderPrimary, thickness: 1)),
|
Expanded(
|
||||||
Padding(
|
child: Divider(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
color: TColors.borderPrimary,
|
||||||
child: Text(
|
thickness: TSizes.dividerHeight,
|
||||||
text,
|
),
|
||||||
style: TextStyle(color: TColors.textSecondary, fontSize: 14),
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
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:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
|
|
||||||
class AuthHeader extends StatelessWidget {
|
class AuthHeader extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
@ -8,6 +10,8 @@ class AuthHeader extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final dark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -15,12 +19,12 @@ class AuthHeader extends StatelessWidget {
|
||||||
title,
|
title,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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 {
|
class OtpInputField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
@ -18,9 +20,13 @@ class OtpInputField extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return SizedBox(
|
||||||
width: 60,
|
width: width,
|
||||||
height: 60,
|
height: height,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
|
@ -32,8 +38,7 @@ class OtpInputField extends StatelessWidget {
|
||||||
LengthLimitingTextInputFormatter(1),
|
LengthLimitingTextInputFormatter(1),
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
],
|
],
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
),
|
),
|
||||||
|
@ -42,15 +47,15 @@ class OtpInputField extends StatelessWidget {
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: TColors.lightContainer,
|
fillColor: TColors.lightContainer,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
borderSide: BorderSide(color: TColors.primary, width: 2),
|
borderSide: BorderSide(color: TColors.primary, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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';
|
import '../../../../shared/widgets/text/custom_text_field.dart';
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ class PasswordField extends StatelessWidget {
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
||||||
color: TColors.textSecondary,
|
color: TColors.textSecondary,
|
||||||
|
size: TSizes.iconMd,
|
||||||
),
|
),
|
||||||
onPressed: onToggleVisibility,
|
onPressed: onToggleVisibility,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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 {
|
class SocialButton extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
|
@ -15,17 +17,18 @@ class SocialButton extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final dark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: TSizes.buttonHeight * 3,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
icon: Icon(icon, color: TColors.textPrimary),
|
icon: Icon(icon, color: TColors.textPrimary, size: TSizes.iconMd),
|
||||||
label: Text(
|
label: Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
color: TColors.textPrimary,
|
color: TColors.textPrimary,
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -33,7 +36,7 @@ class SocialButton extends StatelessWidget {
|
||||||
foregroundColor: TColors.textPrimary,
|
foregroundColor: TColors.textPrimary,
|
||||||
side: BorderSide(color: TColors.borderPrimary),
|
side: BorderSide(color: TColors.borderPrimary),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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/device/device_utility.dart';
|
||||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
||||||
this.controller,
|
this.controller,
|
||||||
this.isScrollable = false,
|
this.isScrollable = false,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.indicatorColor = TColors.primary,
|
this.indicatorColor,
|
||||||
this.automaticIndicatorColorAdjustment = true,
|
this.automaticIndicatorColorAdjustment = true,
|
||||||
this.indicatorWeight = 2.0,
|
this.indicatorWeight = 2.0,
|
||||||
this.indicatorPadding = EdgeInsets.zero,
|
this.indicatorPadding = EdgeInsets.zero,
|
||||||
|
@ -19,10 +20,10 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
||||||
this.indicatorSize,
|
this.indicatorSize,
|
||||||
this.dividerColor,
|
this.dividerColor,
|
||||||
this.dividerHeight,
|
this.dividerHeight,
|
||||||
this.labelColor = TColors.primary,
|
this.labelColor,
|
||||||
this.labelStyle,
|
this.labelStyle,
|
||||||
this.labelPadding,
|
this.labelPadding,
|
||||||
this.unselectedLabelColor = TColors.darkGrey,
|
this.unselectedLabelColor,
|
||||||
this.unselectedLabelStyle,
|
this.unselectedLabelStyle,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.overlayColor,
|
this.overlayColor,
|
||||||
|
@ -68,27 +69,35 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: dark ? TColors.black : TColors.white,
|
color: isDark ? TColors.dark : TColors.white,
|
||||||
child: TabBar(
|
child: TabBar(
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isScrollable: isScrollable,
|
isScrollable: isScrollable,
|
||||||
padding: padding,
|
padding: padding,
|
||||||
indicatorColor: indicatorColor,
|
indicatorColor: indicatorColor ?? TColors.primary,
|
||||||
automaticIndicatorColorAdjustment: automaticIndicatorColorAdjustment,
|
automaticIndicatorColorAdjustment: automaticIndicatorColorAdjustment,
|
||||||
indicatorWeight: indicatorWeight,
|
indicatorWeight: indicatorWeight,
|
||||||
indicatorPadding: indicatorPadding,
|
indicatorPadding: indicatorPadding,
|
||||||
indicator: indicator,
|
indicator: indicator,
|
||||||
indicatorSize: indicatorSize,
|
indicatorSize: indicatorSize,
|
||||||
dividerColor: dividerColor,
|
dividerColor: dividerColor,
|
||||||
dividerHeight: dividerHeight,
|
dividerHeight: dividerHeight ?? TSizes.dividerHeight,
|
||||||
labelColor: labelColor,
|
labelColor: labelColor ?? TColors.primary,
|
||||||
labelStyle: labelStyle,
|
labelStyle:
|
||||||
labelPadding: labelPadding,
|
labelStyle ??
|
||||||
unselectedLabelColor: unselectedLabelColor,
|
Theme.of(
|
||||||
unselectedLabelStyle: unselectedLabelStyle,
|
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,
|
dragStartBehavior: dragStartBehavior,
|
||||||
mouseCursor: mouseCursor,
|
mouseCursor: mouseCursor,
|
||||||
enableFeedback: enableFeedback,
|
enableFeedback: enableFeedback,
|
||||||
|
@ -98,8 +107,19 @@ class TabBarApp extends StatelessWidget implements PreferredSizeWidget {
|
||||||
tabAlignment: tabAlignment,
|
tabAlignment: tabAlignment,
|
||||||
textScaler: textScaler,
|
textScaler: textScaler,
|
||||||
indicatorAnimation: indicatorAnimation,
|
indicatorAnimation: indicatorAnimation,
|
||||||
overlayColor: WidgetStateProperty.all(Colors.blue.shade50),
|
overlayColor:
|
||||||
splashBorderRadius: splashBorderRadius,
|
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:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.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/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 {
|
class StateScreen extends StatelessWidget {
|
||||||
const StateScreen({super.key});
|
const StateScreen({super.key});
|
||||||
|
@ -16,20 +19,22 @@ class StateScreen extends StatelessWidget {
|
||||||
final message = args['message'] as String;
|
final message = args['message'] as String;
|
||||||
final buttonText = args['buttonText'] as String;
|
final buttonText = args['buttonText'] as String;
|
||||||
final onButtonPressed = args['onButtonPressed'] as Function();
|
final onButtonPressed = args['onButtonPressed'] as Function();
|
||||||
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Set system overlay style
|
// Set system overlay style
|
||||||
|
TDeviceUtils.setStatusBarColor(Colors.transparent);
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
SystemUiOverlayStyle(
|
||||||
statusBarColor: Colors.transparent,
|
statusBarColor: Colors.transparent,
|
||||||
statusBarIconBrightness: Brightness.dark,
|
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: TColors.light,
|
backgroundColor: isDark ? TColors.dark : TColors.light,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
@ -39,33 +44,37 @@ class StateScreen extends StatelessWidget {
|
||||||
type == 'success'
|
type == 'success'
|
||||||
? Icons.check_circle_outline
|
? Icons.check_circle_outline
|
||||||
: Icons.error_outline,
|
: Icons.error_outline,
|
||||||
size: 100,
|
size: TSizes.imageThumbSize + TSizes.xl,
|
||||||
color: type == 'success' ? TColors.success : TColors.error,
|
color: type == 'success' ? TColors.success : TColors.error,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: TColors.textPrimary,
|
color: isDark ? TColors.white : TColors.textPrimary,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Message
|
// Message
|
||||||
Text(
|
Text(
|
||||||
message,
|
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,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 48),
|
SizedBox(height: THelperFunctions.screenHeight() * 0.05),
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
AuthButton(text: buttonText, onPressed: onButtonPressed),
|
AuthButton(text: buttonText, onPressed: onButtonPressed),
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.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 {
|
class CustomTextField extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
|
@ -33,18 +35,19 @@ class CustomTextField extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: TColors.textPrimary,
|
color: isDark ? TColors.white : TColors.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: TSizes.sm),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
|
@ -54,42 +57,46 @@ class CustomTextField extends StatelessWidget {
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: TextStyle(color: TColors.textPrimary, fontSize: 16),
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: isDark ? TColors.white : TColors.textPrimary,
|
||||||
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
hintStyle: TextStyle(color: TColors.textSecondary, fontSize: 16),
|
hintStyle: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||||
errorText:
|
errorText:
|
||||||
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: TSizes.md,
|
||||||
vertical: 16,
|
vertical: TSizes.md,
|
||||||
),
|
),
|
||||||
suffixIcon: suffixIcon,
|
suffixIcon: suffixIcon,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: TColors.lightContainer,
|
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.primary, width: 1.5),
|
borderSide: BorderSide(color: TColors.primary, width: 1.5),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.error, width: 1),
|
borderSide: BorderSide(color: TColors.error, width: 1),
|
||||||
),
|
),
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: TColors.error, width: 1.5),
|
borderSide: BorderSide(color: TColors.error, width: 1.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue