Skip to content

Commit 67569f6

Browse files
committed
fix(shopinbit): fix guidelines skip, sticky button, travel form improvements
1 parent 0119410 commit 67569f6

2 files changed

Lines changed: 210 additions & 38 deletions

File tree

lib/pages/shopinbit/shopinbit_step_2.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,24 @@ class _ShopInBitStep2State extends State<ShopInBitStep2> {
5454

5555
void _continue() {
5656
widget.model.category = _selected;
57+
final skipGuidelines = ShopInBitService.instance.loadGuidelinesAccepted();
5758

5859
final skipGuidelines = ShopInBitService.instance.loadGuidelinesAccepted();
5960

6061
if (Util.isDesktop) {
6162
Navigator.of(context, rootNavigator: true).pop();
62-
showDialog<void>(
63-
context: context,
64-
barrierDismissible: false,
65-
builder: (_) => ShopInBitStep3(model: widget.model),
66-
);
63+
if (skipGuidelines) {
64+
widget.model.guidelinesAccepted = true;
65+
Navigator.of(
66+
context,
67+
).pushNamed(ShopInBitStep4.routeName, arguments: widget.model);
68+
} else {
69+
Navigator.of(
70+
context,
71+
).pushNamed(ShopInBitStep3.routeName, arguments: widget.model);
72+
}
6773
} else {
6874
if (skipGuidelines) {
69-
// Returning user — skip guidelines.
7075
widget.model.guidelinesAccepted = true;
7176
Navigator.of(
7277
context,

lib/pages/shopinbit/shopinbit_step_4.dart

Lines changed: 199 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
7878
// Travel-specific controllers
7979
late final TextEditingController _departureCountryController;
8080
late final FocusNode _departureCountryFocusNode;
81+
String? _selectedDepartureCountryIso;
82+
final TextEditingController _departureCountrySearchController =
83+
TextEditingController();
84+
late final TextEditingController _arrangementDetailsController;
85+
late final FocusNode _arrangementDetailsFocusNode;
86+
bool _arrangementDetailsTouched = false;
8187
late final TextEditingController _departureCityController;
8288
late final FocusNode _departureCityFocusNode;
8389
late final TextEditingController _destinationsController;
@@ -251,7 +257,8 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
251257
return !_submitting &&
252258
_privacyAccepted &&
253259
_selectedArrangement != null &&
254-
_departureCountryController.text.trim().isNotEmpty &&
260+
_arrangementDetailsController.text.trim().length >= 10 &&
261+
_selectedDepartureCountryIso != null &&
255262
_departureCityController.text.trim().isNotEmpty &&
256263
(_needsRecommendations ||
257264
_destinationsController.text.trim().isNotEmpty) &&
@@ -338,6 +345,14 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
338345
}
339346
setState(() {});
340347
});
348+
_arrangementDetailsController = TextEditingController();
349+
_arrangementDetailsFocusNode = FocusNode();
350+
_arrangementDetailsFocusNode.addListener(() {
351+
if (!_arrangementDetailsFocusNode.hasFocus) {
352+
_arrangementDetailsTouched = true;
353+
}
354+
setState(() {});
355+
});
341356
_departureCityController = TextEditingController();
342357
_departureCityFocusNode = FocusNode();
343358
_departureCityFocusNode.addListener(() {
@@ -412,6 +427,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
412427
_carBudgetFocusNode.dispose();
413428
_departureCountryController.dispose();
414429
_departureCountryFocusNode.dispose();
430+
_departureCountrySearchController.dispose();
431+
_arrangementDetailsController.dispose();
432+
_arrangementDetailsFocusNode.dispose();
415433
_departureCityController.dispose();
416434
_departureCityFocusNode.dispose();
417435
_destinationsController.dispose();
@@ -483,8 +501,9 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
483501
} else if (widget.model.category == ShopInBitCategory.travel) {
484502
final parts = <String>[
485503
"Arrangement: $_selectedArrangement",
504+
"Details: ${_arrangementDetailsController.text.trim()}",
486505
"Departure: ${_departureCityController.text.trim()}, "
487-
"${_departureCountryController.text.trim()}",
506+
"${_selectedDepartureCountryIso ?? ''}",
488507
];
489508

490509
if (_needsRecommendations) {
@@ -792,6 +811,122 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
792811
);
793812
}
794813

814+
Widget _buildDepartureCountryPicker(bool isDesktop) {
815+
return ClipRRect(
816+
borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius),
817+
child: DropdownButtonHideUnderline(
818+
child: DropdownButton2<String>(
819+
value: _selectedDepartureCountryIso,
820+
items: _countries
821+
.map(
822+
(c) => DropdownMenuItem<String>(
823+
value: c['iso'] as String,
824+
child: Text(
825+
c['label'] as String,
826+
style: isDesktop
827+
? STextStyles.desktopTextExtraSmall(context).copyWith(
828+
color: Theme.of(
829+
context,
830+
).extension<StackColors>()!.textFieldActiveText,
831+
)
832+
: STextStyles.w500_14(context),
833+
),
834+
),
835+
)
836+
.toList(),
837+
onMenuStateChange: (isOpen) {
838+
if (!isOpen) {
839+
_departureCountrySearchController.clear();
840+
}
841+
},
842+
onChanged: _loadingCountries
843+
? null
844+
: (value) {
845+
setState(() {
846+
_selectedDepartureCountryIso = value;
847+
_departureCountryTouched = true;
848+
});
849+
},
850+
hint: Text(
851+
_loadingCountries ? "Loading countries..." : "Departure country",
852+
style: isDesktop
853+
? STextStyles.desktopTextExtraSmall(context).copyWith(
854+
color: Theme.of(
855+
context,
856+
).extension<StackColors>()!.textFieldDefaultSearchIconLeft,
857+
)
858+
: STextStyles.fieldLabel(context),
859+
),
860+
isExpanded: true,
861+
buttonStyleData: ButtonStyleData(
862+
decoration: BoxDecoration(
863+
color: Theme.of(
864+
context,
865+
).extension<StackColors>()!.textFieldDefaultBG,
866+
borderRadius: BorderRadius.circular(
867+
Constants.size.circularBorderRadius,
868+
),
869+
),
870+
),
871+
iconStyleData: IconStyleData(
872+
icon: Padding(
873+
padding: const EdgeInsets.only(right: 10),
874+
child: SvgPicture.asset(
875+
Assets.svg.chevronDown,
876+
width: 12,
877+
height: 6,
878+
color: Theme.of(
879+
context,
880+
).extension<StackColors>()!.textFieldActiveSearchIconRight,
881+
),
882+
),
883+
),
884+
dropdownStyleData: DropdownStyleData(
885+
offset: const Offset(0, 0),
886+
elevation: 0,
887+
maxHeight: 300,
888+
decoration: BoxDecoration(
889+
color: Theme.of(
890+
context,
891+
).extension<StackColors>()!.textFieldDefaultBG,
892+
borderRadius: BorderRadius.circular(
893+
Constants.size.circularBorderRadius,
894+
),
895+
),
896+
),
897+
dropdownSearchData: DropdownSearchData<String>(
898+
searchController: _departureCountrySearchController,
899+
searchInnerWidgetHeight: 48,
900+
searchInnerWidget: TextFormField(
901+
controller: _departureCountrySearchController,
902+
decoration: InputDecoration(
903+
isDense: true,
904+
contentPadding: const EdgeInsets.symmetric(
905+
horizontal: 16,
906+
vertical: 14,
907+
),
908+
hintText: "Search...",
909+
hintStyle: STextStyles.fieldLabel(context),
910+
border: InputBorder.none,
911+
),
912+
),
913+
searchMatchFn: (item, searchValue) {
914+
final label = _countries
915+
.where((c) => c['iso'] == item.value)
916+
.map((c) => c['label'] as String)
917+
.firstOrNull;
918+
return label?.toLowerCase().contains(searchValue.toLowerCase()) ??
919+
false;
920+
},
921+
),
922+
menuItemStyleData: const MenuItemStyleData(
923+
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
924+
),
925+
),
926+
),
927+
);
928+
}
929+
795930
Widget _buildPrivacyCheckbox(bool isDesktop) {
796931
return GestureDetector(
797932
onTap: () {
@@ -907,7 +1042,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
9071042
? STextStyles.desktopTextSmall(context)
9081043
: STextStyles.itemSubtitle(context),
9091044
),
910-
SizedBox(height: isDesktop ? 32 : 24),
1045+
SizedBox(height: isDesktop ? 16 : 12),
9111046

9121047
// What to purchase free-text field
9131048
TextField(
@@ -1099,11 +1234,11 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
10991234
),
11001235
),
11011236
),
1102-
SizedBox(height: isDesktop ? 24 : 16),
1237+
SizedBox(height: isDesktop ? 12 : 12),
11031238

11041239
// Country picker (shared)
11051240
_buildCountryPicker(isDesktop),
1106-
SizedBox(height: isDesktop ? 16 : 12),
1241+
SizedBox(height: isDesktop ? 12 : 12),
11071242

11081243
// Privacy checkbox (shared)
11091244
_buildPrivacyCheckbox(isDesktop),
@@ -1814,19 +1949,12 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
18141949
onChanged: (val) => setState(() => _selectedArrangement = val),
18151950
isDesktop: isDesktop,
18161951
),
1817-
1818-
// === Where ===
1819-
SizedBox(height: isDesktop ? 24 : 16),
1820-
Text(
1821-
"Where",
1822-
style: isDesktop
1823-
? STextStyles.desktopTextSmall(context)
1824-
: STextStyles.w500_14(context),
1825-
),
1826-
SizedBox(height: isDesktop ? 12 : 8),
1952+
SizedBox(height: isDesktop ? 16 : 12),
18271953
TextField(
1828-
controller: _departureCountryController,
1829-
focusNode: _departureCountryFocusNode,
1954+
controller: _arrangementDetailsController,
1955+
focusNode: _arrangementDetailsFocusNode,
1956+
minLines: 3,
1957+
maxLines: 6,
18301958
autocorrect: false,
18311959
enableSuggestions: false,
18321960
onChanged: (_) => setState(() {}),
@@ -1840,8 +1968,8 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
18401968
: STextStyles.field(context),
18411969
decoration:
18421970
standardInputDecoration(
1843-
"Departure country",
1844-
_departureCountryFocusNode,
1971+
"Describe your specific requirements (luggage, cabin class, hotel stars, etc.)",
1972+
_arrangementDetailsFocusNode,
18451973
context,
18461974
desktopMed: isDesktop,
18471975
).copyWith(
@@ -1850,9 +1978,24 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
18501978
horizontal: 16,
18511979
vertical: 12,
18521980
),
1853-
errorText: departureCountryError,
1981+
errorText:
1982+
_arrangementDetailsTouched &&
1983+
_arrangementDetailsController.text.trim().length < 10
1984+
? "Minimum 10 characters"
1985+
: null,
18541986
),
18551987
),
1988+
1989+
// === Where ===
1990+
SizedBox(height: isDesktop ? 24 : 16),
1991+
Text(
1992+
"Where",
1993+
style: isDesktop
1994+
? STextStyles.desktopTextSmall(context)
1995+
: STextStyles.w500_14(context),
1996+
),
1997+
SizedBox(height: isDesktop ? 12 : 8),
1998+
_buildDepartureCountryPicker(isDesktop),
18561999
SizedBox(height: isDesktop ? 16 : 12),
18572000
TextField(
18582001
controller: _departureCityController,
@@ -1971,10 +2114,23 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
19712114
TextField(
19722115
controller: _departureDateController,
19732116
focusNode: _departureDateFocusNode,
1974-
autocorrect: false,
1975-
enableSuggestions: false,
1976-
keyboardType: TextInputType.datetime,
1977-
onChanged: (_) => setState(() {}),
2117+
readOnly: true,
2118+
onTap: () async {
2119+
final picked = await showDatePicker(
2120+
context: context,
2121+
initialDate: DateTime.now(),
2122+
firstDate: DateTime.now(),
2123+
lastDate: DateTime.now().add(const Duration(days: 3650)),
2124+
);
2125+
if (picked != null) {
2126+
final formatted =
2127+
"${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
2128+
setState(() {
2129+
_departureDateController.text = formatted;
2130+
_departureDateTouched = true;
2131+
});
2132+
}
2133+
},
19782134
style: isDesktop
19792135
? STextStyles.desktopTextExtraSmall(context).copyWith(
19802136
color: Theme.of(
@@ -1996,17 +2152,31 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
19962152
vertical: 12,
19972153
),
19982154
labelText: "Departure date",
2155+
suffixIcon: const Icon(Icons.calendar_today, size: 18),
19992156
errorText: departureDateError,
20002157
),
20012158
),
20022159
SizedBox(height: isDesktop ? 16 : 12),
20032160
TextField(
20042161
controller: _returnDateController,
20052162
focusNode: _returnDateFocusNode,
2006-
autocorrect: false,
2007-
enableSuggestions: false,
2008-
keyboardType: TextInputType.datetime,
2009-
onChanged: (_) => setState(() {}),
2163+
readOnly: true,
2164+
onTap: () async {
2165+
final picked = await showDatePicker(
2166+
context: context,
2167+
initialDate: DateTime.now(),
2168+
firstDate: DateTime.now(),
2169+
lastDate: DateTime.now().add(const Duration(days: 3650)),
2170+
);
2171+
if (picked != null) {
2172+
final formatted =
2173+
"${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
2174+
setState(() {
2175+
_returnDateController.text = formatted;
2176+
_returnDateTouched = true;
2177+
});
2178+
}
2179+
},
20102180
style: isDesktop
20112181
? STextStyles.desktopTextExtraSmall(context).copyWith(
20122182
color: Theme.of(
@@ -2028,6 +2198,7 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
20282198
vertical: 12,
20292199
),
20302200
labelText: "Return date",
2201+
suffixIcon: const Icon(Icons.calendar_today, size: 18),
20312202
errorText: returnDateError,
20322203
),
20332204
),
@@ -2070,10 +2241,6 @@ class _ShopInBitStep4State extends State<ShopInBitStep4> {
20702241
"October",
20712242
"November",
20722243
"December",
2073-
"Spring (Mar-May)",
2074-
"Summer (Jun-Aug)",
2075-
"Fall (Sep-Nov)",
2076-
"Winter (Dec-Feb)",
20772244
],
20782245
hint: "Month or season",
20792246
onChanged: (val) => setState(() => _selectedMonthSeason = val),

0 commit comments

Comments
 (0)