709 lines
24 KiB
Dart
709 lines
24 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:real_estate_mobile/core/constants/app_colors.dart';
|
|
import 'package:real_estate_mobile/core/utils/image_url_resolver.dart';
|
|
import 'package:real_estate_mobile/core/widgets/s3_image.dart';
|
|
import 'package:real_estate_mobile/features/profile/data/profile_repository.dart';
|
|
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
|
|
import 'package:real_estate_mobile/features/profile/presentation/widgets/profile_shared_widgets.dart';
|
|
|
|
class ProfileSettingsTab extends ConsumerStatefulWidget {
|
|
const ProfileSettingsTab({super.key});
|
|
|
|
@override
|
|
ConsumerState<ProfileSettingsTab> createState() => _ProfileSettingsTabState();
|
|
}
|
|
|
|
class _ProfileSettingsTabState extends ConsumerState<ProfileSettingsTab> {
|
|
late TextEditingController _fullNameController;
|
|
late TextEditingController _titleController;
|
|
late TextEditingController _emailController;
|
|
late TextEditingController _phoneController;
|
|
|
|
bool _controllersInitialized = false;
|
|
bool _isUploadingAvatar = false;
|
|
File? _localAvatarFile;
|
|
Map<String, String> _savedValues = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fullNameController = TextEditingController();
|
|
_titleController = TextEditingController();
|
|
_emailController = TextEditingController();
|
|
_phoneController = TextEditingController();
|
|
// Listen for changes to enable/disable Cancel & Save buttons
|
|
_fullNameController.addListener(_onFieldChanged);
|
|
_titleController.addListener(_onFieldChanged);
|
|
_phoneController.addListener(_onFieldChanged);
|
|
}
|
|
|
|
void _onFieldChanged() {
|
|
setState(() {}); // triggers rebuild so _hasChanges is re-evaluated
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fullNameController.removeListener(_onFieldChanged);
|
|
_titleController.removeListener(_onFieldChanged);
|
|
_phoneController.removeListener(_onFieldChanged);
|
|
_fullNameController.dispose();
|
|
_titleController.dispose();
|
|
_emailController.dispose();
|
|
_phoneController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _initControllersFromProfile(ProfileState profileState) {
|
|
if (_controllersInitialized) return;
|
|
_controllersInitialized = true;
|
|
|
|
final profile = profileState.profile;
|
|
final isAgent = profileState.isAgent;
|
|
|
|
final firstName = profile['firstName'] as String? ?? '';
|
|
final lastName = profile['lastName'] as String? ?? '';
|
|
_fullNameController.text = '$firstName $lastName'.trim();
|
|
_emailController.text = profile['email'] as String? ??
|
|
(profile['user'] is Map
|
|
? (profile['user'] as Map)['email'] as String? ?? ''
|
|
: '');
|
|
_phoneController.text = profile['phone'] as String? ?? '';
|
|
|
|
if (isAgent) {
|
|
final agentType = profile['agentType'] as Map<String, dynamic>?;
|
|
_titleController.text =
|
|
agentType?['name'] as String? ?? profile['bio'] as String? ?? '';
|
|
} else {
|
|
_titleController.text = profile['headline'] as String? ?? '';
|
|
}
|
|
|
|
_savedValues = {
|
|
'fullName': _fullNameController.text,
|
|
'title': _titleController.text,
|
|
'email': _emailController.text,
|
|
'phone': _phoneController.text,
|
|
};
|
|
}
|
|
|
|
bool get _hasChanges {
|
|
return _fullNameController.text != (_savedValues['fullName'] ?? '') ||
|
|
_titleController.text != (_savedValues['title'] ?? '') ||
|
|
_phoneController.text != (_savedValues['phone'] ?? '');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final profileState = ref.watch(profileProvider);
|
|
|
|
if (!profileState.isLoading && profileState.profile.isNotEmpty) {
|
|
_initControllersFromProfile(profileState);
|
|
}
|
|
|
|
final profile = profileState.profile;
|
|
final isAgent = profileState.isAgent;
|
|
|
|
String? avatarUrl = profile['avatar'] as String?;
|
|
if (avatarUrl == null || avatarUrl.isEmpty) {
|
|
final user = profile['user'] as Map<String, dynamic>?;
|
|
avatarUrl = user?['avatar'] as String?;
|
|
}
|
|
|
|
// Evict stale presigned URL cache and disk cache for the avatar
|
|
// so S3Image always resolves a fresh presigned URL on profile load.
|
|
if (avatarUrl != null && avatarUrl.isNotEmpty) {
|
|
ImageUrlResolver.instance.evict(avatarUrl);
|
|
CachedNetworkImage.evictFromCache(avatarUrl);
|
|
}
|
|
|
|
final descriptionText = isAgent
|
|
? 'This information will be displayed on your public agent page.'
|
|
: 'This information will be displayed on your profile.';
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.fromLTRB(31, 10, 31, 30),
|
|
children: [
|
|
const Text(
|
|
'Public Profile',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
descriptionText,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Avatar
|
|
Center(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 71,
|
|
height: 69,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: AppColors.accentOrange, width: 2),
|
|
),
|
|
child: ClipOval(
|
|
child: _isUploadingAvatar
|
|
? const Center(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: AppColors.accentOrange,
|
|
),
|
|
),
|
|
)
|
|
: _localAvatarFile != null
|
|
? Image.file(_localAvatarFile!,
|
|
width: 71, height: 69, fit: BoxFit.cover)
|
|
: avatarUrl != null && avatarUrl.isNotEmpty
|
|
? S3Image(
|
|
imageUrl: avatarUrl,
|
|
width: 71,
|
|
height: 69,
|
|
fit: BoxFit.cover,
|
|
)
|
|
: const Icon(Icons.person,
|
|
size: 36, color: AppColors.primaryDark),
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: _isUploadingAvatar ? null : _pickAvatar,
|
|
child: Container(
|
|
width: 95,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.accentOrange,
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: _isUploadingAvatar
|
|
? const SizedBox(
|
|
width: 14,
|
|
height: 14,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
)
|
|
: const Text('Upload Now',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
)),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
GestureDetector(
|
|
onTap: _isUploadingAvatar ? null : _deleteAvatar,
|
|
child: Container(
|
|
width: 79,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(7),
|
|
border: Border.all(
|
|
color: AppColors.primaryDark, width: 0.1),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: const Text('Delete',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.primaryDark,
|
|
)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
ProfileFormField(label: 'Full Name', controller: _fullNameController),
|
|
if (isAgent) ...[
|
|
const SizedBox(height: 24),
|
|
ProfileFormField(
|
|
label: 'Career / Agent Type',
|
|
controller: _titleController,
|
|
enabled: false,
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Expanded(
|
|
child: ProfileFormField(
|
|
label: 'Email Address',
|
|
controller: _emailController,
|
|
enabled: false,
|
|
textCapitalization: TextCapitalization.none),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|
child: TextButton(
|
|
onPressed: _openChangeEmailDialog,
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: AppColors.accentOrange,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
),
|
|
child: const Text(
|
|
'Change',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
ProfileFormField(
|
|
label: 'Phone Number',
|
|
controller: _phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
textCapitalization: TextCapitalization.none,
|
|
// Digits only, max 10 (US format) — matches admin panel rule
|
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
|
maxLength: 10,
|
|
),
|
|
const SizedBox(height: 30),
|
|
ProfileActionButtons(
|
|
onSave: _saveProfileSettings,
|
|
onCancel: _resetForm,
|
|
isSaving: profileState.isSaving,
|
|
hasChanges: _hasChanges,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _pickAvatar() async {
|
|
final picker = ImagePicker();
|
|
final picked = await picker.pickImage(
|
|
source: ImageSource.gallery,
|
|
maxWidth: 800,
|
|
maxHeight: 800,
|
|
imageQuality: 85,
|
|
);
|
|
if (picked == null) return;
|
|
|
|
final ext = picked.path.split('.').last.toLowerCase();
|
|
if (!['jpg', 'jpeg', 'png', 'webp'].contains(ext)) {
|
|
if (mounted) _showSnackBar('Please select a JPEG, PNG, or WebP image', isError: true);
|
|
return;
|
|
}
|
|
|
|
final fileBytes = await picked.readAsBytes();
|
|
if (fileBytes.length > 2 * 1024 * 1024) {
|
|
if (mounted) _showSnackBar('Image must be less than 2MB', isError: true);
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_localAvatarFile = File(picked.path);
|
|
_isUploadingAvatar = true;
|
|
});
|
|
|
|
try {
|
|
final profileState = ref.read(profileProvider);
|
|
final role = profileState.isAgent ? 'AGENT' : 'USER';
|
|
final mimeExt = ext == 'jpg' ? 'jpeg' : ext;
|
|
final contentType = 'image/$mimeExt';
|
|
final fileName = picked.name;
|
|
final repo = ProfileRepository();
|
|
|
|
final uploadData =
|
|
await repo.getAvatarUploadUrl(role, fileName, contentType);
|
|
final uploadUrl = uploadData['uploadUrl']!;
|
|
final key = uploadData['key']!;
|
|
|
|
await repo.uploadToS3(uploadUrl, fileBytes, contentType);
|
|
|
|
await ref
|
|
.read(profileProvider.notifier)
|
|
.updateProfile({'avatar': key});
|
|
|
|
if (mounted) _showSnackBar('Avatar updated successfully');
|
|
} catch (e) {
|
|
if (mounted) _showSnackBar('Failed to upload avatar: $e', isError: true);
|
|
setState(() => _localAvatarFile = null);
|
|
} finally {
|
|
if (mounted) setState(() => _isUploadingAvatar = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteAvatar() async {
|
|
final profileState = ref.read(profileProvider);
|
|
final currentAvatar = profileState.profile['avatar'] as String?;
|
|
|
|
if (currentAvatar != null && currentAvatar.isNotEmpty) {
|
|
final repo = ProfileRepository();
|
|
await repo.deleteAvatar(currentAvatar);
|
|
}
|
|
|
|
await ref.read(profileProvider.notifier).updateProfile({'avatar': null});
|
|
|
|
setState(() => _localAvatarFile = null);
|
|
if (mounted) _showSnackBar('Avatar deleted');
|
|
}
|
|
|
|
void _resetForm() {
|
|
if (!_hasChanges) return;
|
|
// Revert to last saved values
|
|
_fullNameController.text = _savedValues['fullName'] ?? '';
|
|
_titleController.text = _savedValues['title'] ?? '';
|
|
_phoneController.text = _savedValues['phone'] ?? '';
|
|
setState(() {});
|
|
}
|
|
|
|
Future<void> _saveProfileSettings() async {
|
|
// Validate phone: must be empty (optional) OR exactly 10 digits
|
|
final phone = _phoneController.text.trim();
|
|
if (phone.isNotEmpty && phone.length != 10) {
|
|
_showSnackBar('Phone number must be exactly 10 digits', isError: true);
|
|
throw Exception('Invalid phone number');
|
|
}
|
|
|
|
final nameParts = _fullNameController.text.trim().split(RegExp(r'\s+'));
|
|
final firstName = nameParts.first;
|
|
final lastName =
|
|
nameParts.length > 1 ? nameParts.sublist(1).join(' ') : '';
|
|
|
|
final data = <String, dynamic>{
|
|
'firstName': firstName,
|
|
'lastName': lastName,
|
|
'phone': phone,
|
|
};
|
|
|
|
await ref.read(profileProvider.notifier).updateProfile(data);
|
|
|
|
// Update saved values so Cancel won't revert after successful save
|
|
if (mounted) {
|
|
setState(() {
|
|
_savedValues = {
|
|
'fullName': _fullNameController.text,
|
|
'title': _titleController.text,
|
|
'email': _emailController.text,
|
|
'phone': _phoneController.text,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
void _showSnackBar(String message, {bool isError = false}) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(message),
|
|
backgroundColor: isError ? Colors.red : const Color(0xFF638559),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _openChangeEmailDialog() async {
|
|
await showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (sheetCtx) {
|
|
return _ChangeEmailSheet(
|
|
onSubmit: (email, password) async {
|
|
final repo = ref.read(profileRepositoryProvider);
|
|
return repo.requestEmailChange(email, password);
|
|
},
|
|
onSuccess: (message) {
|
|
if (mounted) _showSnackBar(message);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Bottom-sheet widget for changing email. Owns its own controllers so they
|
|
/// remain valid throughout the dismiss animation, then disposes cleanly.
|
|
class _ChangeEmailSheet extends StatefulWidget {
|
|
final Future<String> Function(String email, String password) onSubmit;
|
|
final void Function(String message) onSuccess;
|
|
|
|
const _ChangeEmailSheet({
|
|
required this.onSubmit,
|
|
required this.onSuccess,
|
|
});
|
|
|
|
@override
|
|
State<_ChangeEmailSheet> createState() => _ChangeEmailSheetState();
|
|
}
|
|
|
|
class _ChangeEmailSheetState extends State<_ChangeEmailSheet> {
|
|
final _newEmailCtl = TextEditingController();
|
|
final _passwordCtl = TextEditingController();
|
|
String? _localError;
|
|
bool _submitting = false;
|
|
bool _obscurePw = true;
|
|
|
|
@override
|
|
void dispose() {
|
|
_newEmailCtl.dispose();
|
|
_passwordCtl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
final email = _newEmailCtl.text.trim();
|
|
final pw = _passwordCtl.text;
|
|
if (email.isEmpty || pw.isEmpty) {
|
|
setState(() => _localError =
|
|
'Please enter a new email and your current password');
|
|
return;
|
|
}
|
|
setState(() {
|
|
_submitting = true;
|
|
_localError = null;
|
|
});
|
|
try {
|
|
final message = await widget.onSubmit(email, pw);
|
|
if (!mounted) return;
|
|
Navigator.of(context).pop();
|
|
widget.onSuccess(message);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_submitting = false;
|
|
_localError = e.toString().replaceFirst('Exception: ', '');
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: bottomInset),
|
|
child: Container(
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
padding: const EdgeInsets.fromLTRB(24, 12, 24, 24),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// Drag handle
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE5E7EB),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
const Text(
|
|
'Change Email Address',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Text(
|
|
'A verification link will be sent to your new email. Your email changes only after you click the link.',
|
|
style: TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 13,
|
|
color: Color(0xFF6B7280),
|
|
height: 1.4,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_label('New Email'),
|
|
const SizedBox(height: 6),
|
|
TextField(
|
|
controller: _newEmailCtl,
|
|
keyboardType: TextInputType.emailAddress,
|
|
autocorrect: false,
|
|
enableSuggestions: false,
|
|
textCapitalization: TextCapitalization.none,
|
|
autofillHints: const [AutofillHints.email],
|
|
decoration: _decoration('example@re-quest.com'),
|
|
),
|
|
const SizedBox(height: 14),
|
|
_label('Current Password'),
|
|
const SizedBox(height: 6),
|
|
TextField(
|
|
controller: _passwordCtl,
|
|
obscureText: _obscurePw,
|
|
autocorrect: false,
|
|
enableSuggestions: false,
|
|
// Empty autofillHints prevents iOS from showing the
|
|
// "Strong Password" suggestion bar.
|
|
autofillHints: const <String>[],
|
|
decoration: _decoration(
|
|
'Enter your current password',
|
|
suffixIcon: IconButton(
|
|
icon: Icon(
|
|
_obscurePw
|
|
? Icons.visibility_off_outlined
|
|
: Icons.visibility_outlined,
|
|
color: const Color(0xFF6B7280),
|
|
size: 20,
|
|
),
|
|
onPressed: () =>
|
|
setState(() => _obscurePw = !_obscurePw),
|
|
),
|
|
),
|
|
),
|
|
if (_localError != null) ...[
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_localError!,
|
|
style: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
color: Colors.red,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: _submitting
|
|
? null
|
|
: () => Navigator.of(context).pop(),
|
|
style: OutlinedButton.styleFrom(
|
|
side: const BorderSide(color: Color(0xFFE5E7EB)),
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Cancel',
|
|
style: TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: _submitting ? null : _submit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.accentOrange,
|
|
foregroundColor: Colors.white,
|
|
padding:
|
|
const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: Text(
|
|
_submitting ? 'Sending...' : 'Send Link',
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _label(String text) => Text(
|
|
text,
|
|
style: const TextStyle(
|
|
fontFamily: 'Fractul',
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.primaryDark,
|
|
),
|
|
);
|
|
|
|
InputDecoration _decoration(String hint, {Widget? suffixIcon}) {
|
|
return InputDecoration(
|
|
hintText: hint,
|
|
hintStyle: const TextStyle(
|
|
fontFamily: 'SourceSerif4',
|
|
fontSize: 14,
|
|
color: Color(0xFF9CA3AF),
|
|
),
|
|
filled: true,
|
|
fillColor: const Color(0xFFF9FAFB),
|
|
contentPadding:
|
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(color: Color(0xFFE5E7EB)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.accentOrange,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
suffixIcon: suffixIcon,
|
|
);
|
|
}
|
|
}
|