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