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:
vergiLgood1 2025-05-19 15:09:05 +07:00
parent 60fb38da76
commit ba4cbd180a
18 changed files with 1027 additions and 216 deletions

View File

@ -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);
} }

View File

@ -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';

View File

@ -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 = '';
},
), ),
), ),

View File

@ -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,
), ),
), ),

View File

@ -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)),
], ],
); );
} }

View File

@ -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),
], ],
); );
} }

View File

@ -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),
), ),
), ),

View File

@ -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,
), ),

View File

@ -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),
), ),
), ),
), ),

View File

@ -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),
), ),
); );
} }

View File

@ -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,
),
),
);
}),
),
],
);
}
}

View File

@ -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';

View File

@ -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,
);
}
}
}

View File

@ -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,
),
),
),
),
);
}
}

View File

@ -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,
),
),
),
),
),
);
}
}

View File

@ -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,
),
),
);
}),
),
],
);
}
}

View File

@ -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),

View File

@ -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),
], ],
); );
} }