bug fixes done
This commit is contained in:
@@ -47,7 +47,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
child: CircularProgressIndicator(color: Color(0xffF95F62)),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== HANDLE API DATA (LOGGED IN USER) ==========
|
||||
else if (state is MyPassCartApiLoaded) {
|
||||
final apiCartData = state.apiCartData;
|
||||
@@ -73,15 +72,15 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
final String cityName = cartItem.city.cityName;
|
||||
final String cardDisplayName = cartItem.displayCardMode;
|
||||
final String cardTypeName = cartItem.cardMode;
|
||||
final int themeColor =
|
||||
isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
|
||||
final int themeColor = isFlexiCard ? 0xFFF95FAF : 0xFFF95F62;
|
||||
final int adultCount = cartItem.totalAdult;
|
||||
final int childCount = cartItem.totalChild;
|
||||
final int validityDuration = cartItem.noOfDays;
|
||||
final double totalPrice = cartItem.totalAmount.toDouble();
|
||||
|
||||
final bool isUnlimitedCard =
|
||||
cardTypeName.toLowerCase().contains("unlimited");
|
||||
final bool isUnlimitedCard = cardTypeName
|
||||
.toLowerCase()
|
||||
.contains("unlimited");
|
||||
final String validityLabel = isUnlimitedCard
|
||||
? "$validityDuration Days"
|
||||
: "${cartItem.noOfAttractions} Attractions";
|
||||
@@ -111,9 +110,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
bookingId: cartItem.id,
|
||||
couponId: cartItem.couponXid,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
arguments: checkoutData,
|
||||
),
|
||||
settings: RouteSettings(arguments: checkoutData),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -135,7 +132,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== HANDLE LOCAL DATA (NOT LOGGED IN) ==========
|
||||
else if (state is MyPassCartLoaded) {
|
||||
final cartData = state.cartData;
|
||||
@@ -146,8 +142,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
cartData['card_type_name'] as String? ?? '';
|
||||
final String cardDisplayName =
|
||||
cartData['card_display_name'] as String? ?? '';
|
||||
final int themeColor =
|
||||
cartData['theme_color'] as int? ?? 0xFFF95FAF;
|
||||
final int themeColor = cartData['theme_color'] as int? ?? 0xFFF95FAF;
|
||||
final int adultCount = cartData['adult_count'] as int? ?? 0;
|
||||
final int childCount = cartData['child_count'] as int? ?? 0;
|
||||
final double adultPrice =
|
||||
@@ -193,7 +188,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== EMPTY STATE ==========
|
||||
else if (state is MyPassCartEmpty) {
|
||||
return Padding(
|
||||
@@ -221,7 +215,7 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
SizedBox(height: 40.h),
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
label: "Buy a Pass",
|
||||
),
|
||||
@@ -230,7 +224,6 @@ class _MyPassesCartPageState extends State<MyPassesCartPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== ERROR STATE ==========
|
||||
else if (state is MyPassCartError) {
|
||||
return Center(
|
||||
@@ -290,9 +283,7 @@ class _CartItemCard extends StatelessWidget {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(
|
||||
color: Color(themeColor).withOpacity(0.2),
|
||||
),
|
||||
border: Border.all(color: Color(themeColor).withOpacity(0.2)),
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Row(
|
||||
@@ -307,32 +298,32 @@ class _CartItemCard extends StatelessWidget {
|
||||
),
|
||||
child: heroImage.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: heroImage,
|
||||
width: 105.w,
|
||||
height: 130.h,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
placeholder: (context, url) => Image.asset(
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
imageUrl: heroImage,
|
||||
width: 105.w,
|
||||
height: 130.h,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
placeholder: (context, url) => Image.asset(
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
"assets/images/card_banner.png",
|
||||
scale: 4,
|
||||
width: 105.w,
|
||||
height: 123.h,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.66.w),
|
||||
|
||||
@@ -367,7 +358,7 @@ class _CartItemCard extends StatelessWidget {
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text:
|
||||
"$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
|
||||
"$adultCount ${adultCount == 1 ? 'adult' : 'adults'}",
|
||||
color: const Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
@@ -388,7 +379,7 @@ class _CartItemCard extends StatelessWidget {
|
||||
SizedBox(width: 4.w),
|
||||
CustomText(
|
||||
text:
|
||||
"$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
|
||||
"$childCount ${childCount == 1 ? 'Kid' : 'Kids'}",
|
||||
color: const Color(0xFF8E8E8E),
|
||||
size: 12.sp,
|
||||
),
|
||||
@@ -428,10 +419,7 @@ class _CartItemCard extends StatelessWidget {
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "$cardDisplayName ",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.sp),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -443,4 +431,4 @@ class _CartItemCard extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ class _EmptyCartScreen extends StatelessWidget {
|
||||
SizedBox(height: 40.h),
|
||||
CustomFilledButton(
|
||||
onTap: () {
|
||||
|
||||
Navigator.pop(context);
|
||||
},
|
||||
label: "Design my postcard",
|
||||
),
|
||||
|
||||
@@ -13,7 +13,7 @@ class CustomTextField extends StatelessWidget {
|
||||
final TextInputType? keyboardType;
|
||||
final bool obscureText;
|
||||
final Widget? suffixIcon;
|
||||
final Widget? prefixWidget; // ✅ NEW: optional prefix (e.g. CountryCodePicker)
|
||||
final Widget? prefixWidget;
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
final int? maxLength;
|
||||
@@ -40,7 +40,7 @@ class CustomTextField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.obscureText = false,
|
||||
this.suffixIcon,
|
||||
this.prefixWidget, // ✅ NEW
|
||||
this.prefixWidget,
|
||||
this.onChanged,
|
||||
this.maxLength,
|
||||
this.numbersOnly = false,
|
||||
@@ -56,33 +56,26 @@ class CustomTextField extends StatelessWidget {
|
||||
|
||||
void _capitalizeFirstLetter(String value) {
|
||||
if (value.isEmpty) return;
|
||||
|
||||
final capitalized = value[0].toUpperCase() + value.substring(1);
|
||||
|
||||
if (capitalized != value) {
|
||||
controller.value = controller.value.copyWith(
|
||||
text: capitalized,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: capitalized.length,
|
||||
),
|
||||
selection: TextSelection.collapsed(offset: capitalized.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? _internalValidator(String? value) {
|
||||
if (isPreview) return null;
|
||||
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Please enter $label';
|
||||
}
|
||||
|
||||
if (isEmail) {
|
||||
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
||||
if (!emailRegex.hasMatch(value.trim())) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobileNumber) {
|
||||
if (!RegExp(r'^\d+$').hasMatch(value)) {
|
||||
return 'Only numbers are allowed';
|
||||
@@ -91,16 +84,12 @@ class CustomTextField extends StatelessWidget {
|
||||
return 'Mobile number must be $mobileLength digits';
|
||||
}
|
||||
}
|
||||
|
||||
if (noSpace && value.contains(' ')) {
|
||||
return 'Spaces are not allowed';
|
||||
}
|
||||
|
||||
if (noSpecialCharacters &&
|
||||
!RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(value)) {
|
||||
if (noSpecialCharacters && !RegExp(r'^[a-zA-Z0-9\s]+$').hasMatch(value)) {
|
||||
return 'Special characters are not allowed';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -120,32 +109,35 @@ class CustomTextField extends StatelessWidget {
|
||||
if (numbersOnly) {
|
||||
inputFormatters.add(FilteringTextInputFormatter.digitsOnly);
|
||||
}
|
||||
|
||||
if (onlyLetters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z\s]')),
|
||||
);
|
||||
}
|
||||
|
||||
if (noSpecialCharacters) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\s]')),
|
||||
);
|
||||
}
|
||||
|
||||
if (noSpace) {
|
||||
inputFormatters.add(
|
||||
FilteringTextInputFormatter.deny(RegExp(r'\s')),
|
||||
);
|
||||
inputFormatters.add(FilteringTextInputFormatter.deny(RegExp(r'\s')));
|
||||
}
|
||||
|
||||
if (maxLength != null) {
|
||||
inputFormatters.add(LengthLimitingTextInputFormatter(maxLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Determine border radius — if prefixWidget is present, only round the right side
|
||||
final fillColor = isPreview
|
||||
? Colors.grey.shade100
|
||||
: enabled
|
||||
? const Color(0xFFFFF5F5)
|
||||
: Colors.grey.shade200;
|
||||
|
||||
final borderColor = const Color(0xBBC83B61).withOpacity(0.4);
|
||||
|
||||
// ✅ Full radius used for normal fields
|
||||
// ✅ Only right-side radius when prefix is present (left side is the prefix container)
|
||||
final borderRadius = prefixWidget != null
|
||||
? BorderRadius.only(
|
||||
topRight: Radius.circular(8.r),
|
||||
@@ -153,127 +145,297 @@ class CustomTextField extends StatelessWidget {
|
||||
)
|
||||
: BorderRadius.circular(8.r);
|
||||
|
||||
// ✅ Determine fill color
|
||||
final fillColor = isPreview
|
||||
? Colors.grey.shade100
|
||||
: enabled
|
||||
? const Color(0xFFFFF5F5)
|
||||
: Colors.grey.shade200;
|
||||
|
||||
final textFormField = TextFormField(
|
||||
controller: controller,
|
||||
maxLines: obscureText ? 1 : maxLines,
|
||||
enabled: isPreview ? false : enabled,
|
||||
obscureText: obscureText,
|
||||
validator: validator ?? _internalValidator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
keyboardType: keyboardType ??
|
||||
(isMobileNumber
|
||||
? TextInputType.phone
|
||||
: isEmail
|
||||
? TextInputType.emailAddress
|
||||
: TextInputType.name),
|
||||
inputFormatters: inputFormatters,
|
||||
onChanged: (value) {
|
||||
if (isFirstLetterCapital) {
|
||||
_capitalizeFirstLetter(value);
|
||||
}
|
||||
if (onChanged != null) {
|
||||
onChanged!(value);
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
counterText: "",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: fillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
// ✅ Reduce left padding when prefixWidget takes up the left side
|
||||
horizontal: prefixWidget != null ? 12.w : 24.w,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xBBC83B61).withOpacity(0.4),
|
||||
width: .4.w,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xFFF95F62),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red,
|
||||
width: 1.5.w,
|
||||
),
|
||||
),
|
||||
errorStyle: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.red,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 14.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ✅ Only show label row if label is not empty
|
||||
// Label
|
||||
if (label.isNotEmpty) ...[
|
||||
CustomText(text: label, size: 14.sp),
|
||||
SizedBox(height: 6.h),
|
||||
],
|
||||
|
||||
// ✅ If prefixWidget provided, wrap it in a Row with the picker on the left
|
||||
if (prefixWidget != null)
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Prefix container — styled to match the field
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: fillColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.r),
|
||||
bottomLeft: Radius.circular(8.r),
|
||||
),
|
||||
border: Border.all(
|
||||
color: const Color(0xBBC83B61).withOpacity(0.4),
|
||||
width: 0.4.w,
|
||||
),
|
||||
),
|
||||
child: prefixWidget!,
|
||||
),
|
||||
// TextField takes the remaining space
|
||||
Expanded(child: textFormField),
|
||||
],
|
||||
),
|
||||
// ✅ THE CORE FIX:
|
||||
// We split the phone field into two parts:
|
||||
// 1. The input ROW (prefix + text field) — wrapped in IntrinsicHeight
|
||||
// so both sides match height perfectly
|
||||
// 2. The error text — rendered OUTSIDE and BELOW the row
|
||||
// so IntrinsicHeight is never affected by error text height
|
||||
_PrefixFieldWithError(
|
||||
prefixWidget: prefixWidget!,
|
||||
fillColor: fillColor,
|
||||
borderColor: borderColor,
|
||||
borderRadius: borderRadius,
|
||||
maxLines: maxLines,
|
||||
obscureText: obscureText,
|
||||
enabled: isPreview ? false : enabled,
|
||||
controller: controller,
|
||||
validator: validator ?? _internalValidator,
|
||||
keyboardType: keyboardType ?? TextInputType.phone,
|
||||
inputFormatters: inputFormatters,
|
||||
hint: hint,
|
||||
onChanged: (value) {
|
||||
if (isFirstLetterCapital) _capitalizeFirstLetter(value);
|
||||
if (onChanged != null) onChanged!(value);
|
||||
},
|
||||
suffixIcon: suffixIcon,
|
||||
)
|
||||
else
|
||||
textFormField,
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
maxLines: obscureText ? 1 : maxLines,
|
||||
enabled: isPreview ? false : enabled,
|
||||
obscureText: obscureText,
|
||||
validator: validator ?? _internalValidator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
keyboardType: keyboardType ??
|
||||
(isMobileNumber
|
||||
? TextInputType.phone
|
||||
: isEmail
|
||||
? TextInputType.emailAddress
|
||||
: TextInputType.name),
|
||||
inputFormatters: inputFormatters,
|
||||
onChanged: (value) {
|
||||
if (isFirstLetterCapital) _capitalizeFirstLetter(value);
|
||||
if (onChanged != null) onChanged!(value);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
counterText: "",
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: fillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24.w,
|
||||
vertical: maxLines != null && maxLines! > 1 ? 12.h : 10.h,
|
||||
),
|
||||
suffixIcon: suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: borderColor, width: .4.w),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: const Color(0xFFF95F62), width: 1.w),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: Colors.red, width: 1.w),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadius,
|
||||
borderSide: BorderSide(color: Colors.red, width: 1.5.w),
|
||||
),
|
||||
errorStyle: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.red,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ Separate StatefulWidget for the prefix + field combo.
|
||||
/// It manually manages validation state and renders error text
|
||||
/// OUTSIDE the IntrinsicHeight row — this is the key to perfect alignment.
|
||||
class _PrefixFieldWithError extends StatefulWidget {
|
||||
final Widget prefixWidget;
|
||||
final Color fillColor;
|
||||
final Color borderColor;
|
||||
final BorderRadius borderRadius;
|
||||
final int? maxLines;
|
||||
final bool obscureText;
|
||||
final bool enabled;
|
||||
final TextEditingController controller;
|
||||
final String? Function(String?)? validator;
|
||||
final TextInputType keyboardType;
|
||||
final List<TextInputFormatter> inputFormatters;
|
||||
final String hint;
|
||||
final void Function(String) onChanged;
|
||||
final Widget? suffixIcon;
|
||||
|
||||
const _PrefixFieldWithError({
|
||||
required this.prefixWidget,
|
||||
required this.fillColor,
|
||||
required this.borderColor,
|
||||
required this.borderRadius,
|
||||
required this.maxLines,
|
||||
required this.obscureText,
|
||||
required this.enabled,
|
||||
required this.controller,
|
||||
required this.validator,
|
||||
required this.keyboardType,
|
||||
required this.inputFormatters,
|
||||
required this.hint,
|
||||
required this.onChanged,
|
||||
required this.suffixIcon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PrefixFieldWithError> createState() => _PrefixFieldWithErrorState();
|
||||
}
|
||||
|
||||
class _PrefixFieldWithErrorState extends State<_PrefixFieldWithError> {
|
||||
String? _errorText;
|
||||
bool _hasInteracted = false;
|
||||
|
||||
void _validate(String value) {
|
||||
if (!_hasInteracted) return;
|
||||
setState(() {
|
||||
_errorText = widget.validator?.call(value);
|
||||
});
|
||||
}
|
||||
|
||||
void _onChanged(String value) {
|
||||
setState(() => _hasInteracted = true);
|
||||
_validate(value);
|
||||
widget.onChanged(value);
|
||||
}
|
||||
|
||||
// Called by Form.validate() via FormField
|
||||
String? _formValidator(String? value) {
|
||||
setState(() => _hasInteracted = true);
|
||||
final error = widget.validator?.call(value);
|
||||
// Update error text after frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _errorText = error);
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
bool get _hasError => _errorText != null && _errorText!.isNotEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = widget.borderColor;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ✅ IntrinsicHeight ONLY wraps the input row (prefix + field)
|
||||
// Error text is outside this, so IntrinsicHeight height is never affected by it
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Prefix container — matches field height perfectly via IntrinsicHeight
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.fillColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8.r),
|
||||
bottomLeft: Radius.circular(8.r),
|
||||
),
|
||||
// ✅ No right border — avoids double border line
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: _hasError ? Colors.red : borderColor,
|
||||
width: _hasError ? 1.w : 0.4.w,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: _hasError ? Colors.red : borderColor,
|
||||
width: _hasError ? 1.w : 0.4.w,
|
||||
),
|
||||
left: BorderSide(
|
||||
color: _hasError ? Colors.red : borderColor,
|
||||
width: _hasError ? 1.w : 0.4.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: widget.prefixWidget,
|
||||
),
|
||||
|
||||
// Text field — takes remaining width
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: widget.controller,
|
||||
maxLines: widget.obscureText ? 1 : widget.maxLines,
|
||||
enabled: widget.enabled,
|
||||
obscureText: widget.obscureText,
|
||||
validator: _formValidator,
|
||||
// ✅ No autovalidateMode here — we handle it manually
|
||||
// so we can show error text outside the row
|
||||
autovalidateMode: AutovalidateMode.disabled,
|
||||
keyboardType: widget.keyboardType,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
onChanged: _onChanged,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hint,
|
||||
counterText: "",
|
||||
// ✅ errorText: null always — we render error ourselves below
|
||||
errorText: null,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xFF8E8E8E),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: widget.fillColor,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w,
|
||||
vertical: 10.h,
|
||||
),
|
||||
suffixIcon: widget.suffixIcon,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: widget.borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: _hasError ? Colors.red : borderColor,
|
||||
width: _hasError ? 1.w : 0.4.w,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: widget.borderRadius,
|
||||
borderSide: BorderSide(
|
||||
color: _hasError
|
||||
? Colors.red
|
||||
: const Color(0xFFF95F62),
|
||||
width: 1.w,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: widget.borderRadius,
|
||||
borderSide:
|
||||
BorderSide(color: Colors.red, width: 1.w),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: widget.borderRadius,
|
||||
borderSide:
|
||||
BorderSide(color: Colors.red, width: 1.5.w),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ✅ Error text rendered OUTSIDE the IntrinsicHeight row
|
||||
// This is why the prefix box never grows when error appears
|
||||
if (_hasError) ...[
|
||||
SizedBox(height: 4.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 4.w),
|
||||
child: Text(
|
||||
_errorText!,
|
||||
style: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color: Colors.red,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +328,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
label: "City *",
|
||||
hint: "Enter your city",
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
// noSpace: true,
|
||||
controller: cityController,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
@@ -341,7 +341,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
label: "State *",
|
||||
hint: "Enter your state",
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
// noSpace: true,
|
||||
controller: stateController,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
@@ -354,7 +354,7 @@ class _CreateAccountViewState extends State<CreateAccountView> {
|
||||
label: "Country *",
|
||||
hint: "Enter your country",
|
||||
maxLength: 50,
|
||||
noSpace: true,
|
||||
// noSpace: true,
|
||||
controller: countryController,
|
||||
isFirstLetterCapital: true,
|
||||
),
|
||||
|
||||
@@ -90,6 +90,7 @@ class Attraction {
|
||||
final num? ticketPriceChild;
|
||||
final String? bookingEmail;
|
||||
final String? bookingPhoneNumber;
|
||||
final bool isBookingRequired; // ✅ added
|
||||
final String image;
|
||||
|
||||
Attraction({
|
||||
@@ -100,6 +101,7 @@ class Attraction {
|
||||
this.ticketPriceChild,
|
||||
this.bookingEmail,
|
||||
this.bookingPhoneNumber,
|
||||
required this.isBookingRequired,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
@@ -112,6 +114,7 @@ class Attraction {
|
||||
ticketPriceChild: json?['ticketPriceChild'],
|
||||
bookingEmail: json?['bookingEmail'],
|
||||
bookingPhoneNumber: json?['bookingPhoneNumber'],
|
||||
isBookingRequired: json?['isBookingRequired'] ?? false, // ✅ safe
|
||||
image: json?['image'] ?? '',
|
||||
);
|
||||
}
|
||||
@@ -125,6 +128,7 @@ class Attraction {
|
||||
'ticketPriceChild': ticketPriceChild,
|
||||
'bookingEmail': bookingEmail,
|
||||
'bookingPhoneNumber': bookingPhoneNumber,
|
||||
'isBookingRequired': isBookingRequired,
|
||||
'image': image,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -241,9 +241,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
image: attraction.image,
|
||||
ticketPriceAdult: attraction.ticketPriceAdult,
|
||||
ticketPriceChild: attraction.ticketPriceChild,
|
||||
bookingEmail: attraction.bookingEmail,
|
||||
bookingPhoneNumber:
|
||||
attraction.bookingPhoneNumber,
|
||||
isBookingRequired: attraction.isBookingRequired,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -255,8 +253,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
image: '',
|
||||
ticketPriceAdult: null,
|
||||
ticketPriceChild: null,
|
||||
bookingEmail: null,
|
||||
bookingPhoneNumber: null,
|
||||
isBookingRequired: false,
|
||||
),
|
||||
],
|
||||
SizedBox(height: 16.h),
|
||||
@@ -419,14 +416,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
required String image,
|
||||
num? ticketPriceAdult,
|
||||
num? ticketPriceChild,
|
||||
String? bookingEmail,
|
||||
String? bookingPhoneNumber,
|
||||
required bool isBookingRequired,
|
||||
}) {
|
||||
// Check if booking is required (both email and phone are empty/null)
|
||||
final bool isBookingRequired =
|
||||
(bookingEmail == null || bookingEmail.isEmpty) &&
|
||||
(bookingPhoneNumber == null || bookingPhoneNumber.isEmpty);
|
||||
|
||||
// Format the price display
|
||||
String priceText = ticketPriceAdult != null
|
||||
? "\$$ticketPriceAdult/person"
|
||||
@@ -440,36 +431,34 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
/// 🔥 Attraction Image (Real Image Style Box)
|
||||
/// 🔥 Attraction Image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12.r),
|
||||
child: image.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: image,
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
|
||||
placeholder: (context, url) => Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
imageUrl: image,
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
errorWidget: (context, url, error) => Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
)
|
||||
: Image.asset(
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
"assets/images/aa4.png",
|
||||
height: 100.w,
|
||||
width: 90.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(width: 12.w),
|
||||
@@ -485,6 +474,8 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14.sp,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
SizedBox(height: 2.h),
|
||||
@@ -511,7 +502,7 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
|
||||
SizedBox(height: 6.h),
|
||||
|
||||
// Show "Booking Required" tag only if both email and phone are null/empty
|
||||
/// 🔥 Booking Required Tag
|
||||
if (isBookingRequired)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -519,14 +510,16 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
vertical: 4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
color: const Color(0xffC1D2F8),
|
||||
border: Border.all(color: const Color(0xff2563EB)),
|
||||
borderRadius: BorderRadius.circular(20.r),
|
||||
),
|
||||
child: Text(
|
||||
"Booking Required",
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10.sp,
|
||||
color: Colors.blue.shade700,
|
||||
fontSize: 11.sp,
|
||||
color: const Color(0xff1A1A1A),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -536,12 +529,12 @@ class _PassDetailsViewState extends State<PassDetailsView> {
|
||||
|
||||
SizedBox(width: 8.w),
|
||||
|
||||
/// 🔥 QR Code Circle (Proper UI like Design)
|
||||
/// 🔥 QR Code Circle
|
||||
Container(
|
||||
height: 44.w,
|
||||
width: 44.w,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF8EDED), // light pink circle bg
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF8EDED),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Padding(
|
||||
|
||||
@@ -23,10 +23,11 @@ class PassAttractionCard extends StatelessWidget {
|
||||
final String imageUrl = attraction.coverImageUrl;
|
||||
|
||||
/// Show "Booking Required" when both email and phone are empty/null
|
||||
final bool showBookingRequired =
|
||||
(attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) ||
|
||||
(attraction.bookingPhoneNumber.isEmpty ||
|
||||
attraction.bookingPhoneNumber == null);
|
||||
// final bool showBookingRequired =
|
||||
// (attraction.bookingEmail.isEmpty || attraction.bookingEmail == null) ||
|
||||
// (attraction.bookingPhoneNumber.isEmpty ||
|
||||
// attraction.bookingPhoneNumber == null);
|
||||
final bool showBookingRequired = attraction.isBookingRequired;
|
||||
|
||||
/// Format the price display
|
||||
String priceText = attraction.ticketPriceAdult != null
|
||||
|
||||
@@ -692,7 +692,7 @@ class _EditProfilePageState extends State<EditProfilePage> {
|
||||
controller: cityController,
|
||||
enabled: !isLoading,
|
||||
maxLength: 50,
|
||||
onlyLetters: true,
|
||||
// onlyLetters: true,
|
||||
isPreview: true,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -68,6 +68,9 @@ class _YourItineraryViewState extends State<YourItineraryView> {
|
||||
final file = File('${dir.path}/itinerary_${widget.itineraryId}.pdf');
|
||||
await file.writeAsBytes(state.pdfBytes);
|
||||
await OpenFilex.open(file.path);
|
||||
// ScaffoldMessenger.of(context).showSnackBar(
|
||||
// const SnackBar(content: Text('PDF downloaded successfully!')),
|
||||
// );
|
||||
} else if (state is DownloadItineraryPdfFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage)),
|
||||
|
||||
Reference in New Issue
Block a user