@devtravelcode/widget-native 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/LICENSE +18 -0
  2. package/dist/App.d.ts +10 -0
  3. package/dist/App.js +192 -0
  4. package/dist/TravelHubWidget.d.ts +12 -0
  5. package/dist/TravelHubWidget.js +58 -0
  6. package/dist/components/ErrorBoundary.d.ts +18 -0
  7. package/dist/components/ErrorBoundary.js +63 -0
  8. package/dist/components/ui/Modal.d.ts +11 -0
  9. package/dist/components/ui/Modal.js +66 -0
  10. package/dist/components/ui/Skeleton.d.ts +8 -0
  11. package/dist/components/ui/Skeleton.js +33 -0
  12. package/dist/components/ui/badge.d.ts +11 -0
  13. package/dist/components/ui/badge.js +54 -0
  14. package/dist/components/ui/button.d.ts +14 -0
  15. package/dist/components/ui/button.js +101 -0
  16. package/dist/components/ui/card.d.ts +16 -0
  17. package/dist/components/ui/card.js +51 -0
  18. package/dist/components/ui/input.d.ts +8 -0
  19. package/dist/components/ui/input.js +61 -0
  20. package/dist/components/ui/label.d.ts +8 -0
  21. package/dist/components/ui/label.js +17 -0
  22. package/dist/context/WidgetContext.d.ts +29 -0
  23. package/dist/context/WidgetContext.js +64 -0
  24. package/dist/icons/CarIcon.d.ts +4 -0
  25. package/dist/icons/CarIcon.js +3 -0
  26. package/dist/icons/CloseIcon.d.ts +7 -0
  27. package/dist/icons/CloseIcon.js +3 -0
  28. package/dist/icons/FlightIcon.d.ts +4 -0
  29. package/dist/icons/FlightIcon.js +3 -0
  30. package/dist/icons/HotelIcon.d.ts +4 -0
  31. package/dist/icons/HotelIcon.js +3 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/utils.d.ts +2 -0
  35. package/dist/lib/utils.js +3 -0
  36. package/dist/locales/widgetTranslations.d.ts +1 -0
  37. package/dist/locales/widgetTranslations.js +732 -0
  38. package/dist/modules/hooks/useAirportSearch.d.ts +21 -0
  39. package/dist/modules/hooks/useAirportSearch.js +48 -0
  40. package/dist/modules/hooks/useCarOffers.d.ts +7 -0
  41. package/dist/modules/hooks/useCarOffers.js +47 -0
  42. package/dist/modules/hooks/useCountriesByCodes.d.ts +4 -0
  43. package/dist/modules/hooks/useCountriesByCodes.js +60 -0
  44. package/dist/modules/hooks/useCountriesSearch.d.ts +11 -0
  45. package/dist/modules/hooks/useCountriesSearch.js +55 -0
  46. package/dist/modules/hooks/useDefaultHotelLocation.d.ts +7 -0
  47. package/dist/modules/hooks/useDefaultHotelLocation.js +44 -0
  48. package/dist/modules/hooks/useDetectNationality.d.ts +1 -0
  49. package/dist/modules/hooks/useDetectNationality.js +20 -0
  50. package/dist/modules/hooks/useFlightSearch.d.ts +20 -0
  51. package/dist/modules/hooks/useFlightSearch.js +243 -0
  52. package/dist/modules/hooks/useHotelDetails.d.ts +7 -0
  53. package/dist/modules/hooks/useHotelDetails.js +80 -0
  54. package/dist/modules/hooks/useHotelLocations.d.ts +5 -0
  55. package/dist/modules/hooks/useHotelLocations.js +42 -0
  56. package/dist/modules/hooks/useHotelMapData.d.ts +12 -0
  57. package/dist/modules/hooks/useHotelMapData.js +78 -0
  58. package/dist/modules/hooks/useHotelOffers.d.ts +11 -0
  59. package/dist/modules/hooks/useHotelOffers.js +122 -0
  60. package/dist/modules/hooks/useHotelSearchSSE.d.ts +18 -0
  61. package/dist/modules/hooks/useHotelSearchSSE.js +411 -0
  62. package/dist/modules/hooks/useSearchExpiration.d.ts +6 -0
  63. package/dist/modules/hooks/useSearchExpiration.js +35 -0
  64. package/dist/modules/hooks/useTranslationsHub.d.ts +5 -0
  65. package/dist/modules/hooks/useTranslationsHub.js +48 -0
  66. package/dist/modules/navbar/Navbar.d.ts +8 -0
  67. package/dist/modules/navbar/Navbar.js +69 -0
  68. package/dist/modules/search-form/AgeSelector.d.ts +9 -0
  69. package/dist/modules/search-form/AgeSelector.js +109 -0
  70. package/dist/modules/search-form/CustomSelect.d.ts +14 -0
  71. package/dist/modules/search-form/CustomSelect.js +94 -0
  72. package/dist/modules/search-form/FiltersSkeleton.d.ts +6 -0
  73. package/dist/modules/search-form/FiltersSkeleton.js +35 -0
  74. package/dist/modules/search-form/NationalityInput.d.ts +18 -0
  75. package/dist/modules/search-form/NationalityInput.js +172 -0
  76. package/dist/modules/search-form/Pagination.d.ts +8 -0
  77. package/dist/modules/search-form/Pagination.js +84 -0
  78. package/dist/modules/search-form/SearchExpiredPopup.d.ts +10 -0
  79. package/dist/modules/search-form/SearchExpiredPopup.js +91 -0
  80. package/dist/modules/search-form/SkeletonWidget.d.ts +2 -0
  81. package/dist/modules/search-form/SkeletonWidget.js +33 -0
  82. package/dist/modules/search-form/TimePicker.d.ts +9 -0
  83. package/dist/modules/search-form/TimePicker.js +182 -0
  84. package/dist/modules/search-form/cars/CarEmptyState.d.ts +6 -0
  85. package/dist/modules/search-form/cars/CarEmptyState.js +42 -0
  86. package/dist/modules/search-form/cars/CarFiltersSkeleton.d.ts +6 -0
  87. package/dist/modules/search-form/cars/CarFiltersSkeleton.js +35 -0
  88. package/dist/modules/search-form/cars/CarOffersList.d.ts +43 -0
  89. package/dist/modules/search-form/cars/CarOffersList.js +214 -0
  90. package/dist/modules/search-form/cars/CarResults.d.ts +25 -0
  91. package/dist/modules/search-form/cars/CarResults.js +116 -0
  92. package/dist/modules/search-form/cars/CarSearchForm.d.ts +19 -0
  93. package/dist/modules/search-form/cars/CarSearchForm.js +173 -0
  94. package/dist/modules/search-form/cars/CarSkeleton.d.ts +6 -0
  95. package/dist/modules/search-form/cars/CarSkeleton.js +46 -0
  96. package/dist/modules/search-form/cars/CarTopOffersSlider.d.ts +20 -0
  97. package/dist/modules/search-form/cars/CarTopOffersSlider.js +100 -0
  98. package/dist/modules/search-form/cars/car-filters/CarFilters.d.ts +8 -0
  99. package/dist/modules/search-form/cars/car-filters/CarFilters.js +318 -0
  100. package/dist/modules/search-form/cars/car-filters/FilterSection.d.ts +9 -0
  101. package/dist/modules/search-form/cars/car-filters/FilterSection.js +30 -0
  102. package/dist/modules/search-form/cars/car-filters/index.d.ts +6 -0
  103. package/dist/modules/search-form/cars/car-filters/index.js +3 -0
  104. package/dist/modules/search-form/cars/car-filters/useCarFilters.d.ts +24 -0
  105. package/dist/modules/search-form/cars/car-filters/useCarFilters.js +121 -0
  106. package/dist/modules/search-form/flight/AirlinesSelector.d.ts +8 -0
  107. package/dist/modules/search-form/flight/AirlinesSelector.js +235 -0
  108. package/dist/modules/search-form/flight/CabinClassSelector.d.ts +9 -0
  109. package/dist/modules/search-form/flight/CabinClassSelector.js +63 -0
  110. package/dist/modules/search-form/flight/DatePicker.d.ts +12 -0
  111. package/dist/modules/search-form/flight/DatePicker.js +536 -0
  112. package/dist/modules/search-form/flight/FlightSearchForm.d.ts +18 -0
  113. package/dist/modules/search-form/flight/FlightSearchForm.js +147 -0
  114. package/dist/modules/search-form/flight/LocationInput.d.ts +11 -0
  115. package/dist/modules/search-form/flight/LocationInput.js +294 -0
  116. package/dist/modules/search-form/flight/PassengersPicker.d.ts +16 -0
  117. package/dist/modules/search-form/flight/PassengersPicker.js +203 -0
  118. package/dist/modules/search-form/flight/SwitchLocationBtn.d.ts +9 -0
  119. package/dist/modules/search-form/flight/SwitchLocationBtn.js +48 -0
  120. package/dist/modules/search-form/flight/flight-filters/FiltersContent.d.ts +21 -0
  121. package/dist/modules/search-form/flight/flight-filters/FiltersContent.js +145 -0
  122. package/dist/modules/search-form/flight/flight-filters/FlightFiltersDynamic.d.ts +11 -0
  123. package/dist/modules/search-form/flight/flight-filters/FlightFiltersDynamic.js +213 -0
  124. package/dist/modules/search-form/flight/flight-filters/RangeBlock.d.ts +22 -0
  125. package/dist/modules/search-form/flight/flight-filters/RangeBlock.js +164 -0
  126. package/dist/modules/search-form/flight/flight-filters/index.d.ts +1 -0
  127. package/dist/modules/search-form/flight/flight-filters/index.js +1 -0
  128. package/dist/modules/search-form/flight/flight-results/FlightAdditionalInfo.d.ts +16 -0
  129. package/dist/modules/search-form/flight/flight-results/FlightAdditionalInfo.js +229 -0
  130. package/dist/modules/search-form/flight/flight-results/FlightBaggageToggle.d.ts +15 -0
  131. package/dist/modules/search-form/flight/flight-results/FlightBaggageToggle.js +110 -0
  132. package/dist/modules/search-form/flight/flight-results/FlightCard.d.ts +14 -0
  133. package/dist/modules/search-form/flight/flight-results/FlightCard.js +436 -0
  134. package/dist/modules/search-form/flight/flight-results/FlightInfo.d.ts +12 -0
  135. package/dist/modules/search-form/flight/flight-results/FlightInfo.js +48 -0
  136. package/dist/modules/search-form/flight/flight-results/FlightPriceBlock.d.ts +11 -0
  137. package/dist/modules/search-form/flight/flight-results/FlightPriceBlock.js +36 -0
  138. package/dist/modules/search-form/flight/flight-results/FlightResults.d.ts +22 -0
  139. package/dist/modules/search-form/flight/flight-results/FlightResults.js +109 -0
  140. package/dist/modules/search-form/flight/flight-results/FlightSegments.d.ts +40 -0
  141. package/dist/modules/search-form/flight/flight-results/FlightSegments.js +162 -0
  142. package/dist/modules/search-form/flight/flight-results/FlightsSkeleton.d.ts +6 -0
  143. package/dist/modules/search-form/flight/flight-results/FlightsSkeleton.js +52 -0
  144. package/dist/modules/search-form/flight/flight-results/ProvidersLoader.d.ts +9 -0
  145. package/dist/modules/search-form/flight/flight-results/ProvidersLoader.js +242 -0
  146. package/dist/modules/search-form/hotel/DatePicker.d.ts +1 -0
  147. package/dist/modules/search-form/hotel/DatePicker.js +1 -0
  148. package/dist/modules/search-form/hotel/GuestSelector.d.ts +13 -0
  149. package/dist/modules/search-form/hotel/GuestSelector.js +272 -0
  150. package/dist/modules/search-form/hotel/GuestSelectorMobilePopup.d.ts +2 -0
  151. package/dist/modules/search-form/hotel/GuestSelectorMobilePopup.js +1 -0
  152. package/dist/modules/search-form/hotel/HotelLocation.d.ts +17 -0
  153. package/dist/modules/search-form/hotel/HotelLocation.js +178 -0
  154. package/dist/modules/search-form/hotel/HotelLocationMobilePopup.d.ts +2 -0
  155. package/dist/modules/search-form/hotel/HotelLocationMobilePopup.js +1 -0
  156. package/dist/modules/search-form/hotel/HotelResults.d.ts +21 -0
  157. package/dist/modules/search-form/hotel/HotelResults.js +333 -0
  158. package/dist/modules/search-form/hotel/HotelSearchForm.d.ts +13 -0
  159. package/dist/modules/search-form/hotel/HotelSearchForm.js +130 -0
  160. package/dist/modules/search-form/hotel/HotelsSkeleton.d.ts +6 -0
  161. package/dist/modules/search-form/hotel/HotelsSkeleton.js +40 -0
  162. package/dist/modules/search-form/hotel/PassengersPicker.d.ts +2 -0
  163. package/dist/modules/search-form/hotel/PassengersPicker.js +1 -0
  164. package/dist/modules/search-form/hotel/components/HotelCard.d.ts +11 -0
  165. package/dist/modules/search-form/hotel/components/HotelCard.js +180 -0
  166. package/dist/modules/search-form/hotel/components/HotelEmptyState.d.ts +6 -0
  167. package/dist/modules/search-form/hotel/components/HotelEmptyState.js +34 -0
  168. package/dist/modules/search-form/hotel/components/HotelGallery.d.ts +6 -0
  169. package/dist/modules/search-form/hotel/components/HotelGallery.js +77 -0
  170. package/dist/modules/search-form/hotel/components/HotelList.d.ts +9 -0
  171. package/dist/modules/search-form/hotel/components/HotelList.js +12 -0
  172. package/dist/modules/search-form/hotel/components/HotelMap.d.ts +26 -0
  173. package/dist/modules/search-form/hotel/components/HotelMap.js +707 -0
  174. package/dist/modules/search-form/hotel/components/HotelPin.d.ts +7 -0
  175. package/dist/modules/search-form/hotel/components/HotelPin.js +1 -0
  176. package/dist/modules/search-form/hotel/hotel-filters/HotelFilters.d.ts +29 -0
  177. package/dist/modules/search-form/hotel/hotel-filters/HotelFilters.js +339 -0
  178. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersBar.d.ts +3 -0
  179. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersBar.js +2 -0
  180. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersContent.d.ts +2 -0
  181. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersContent.js +1 -0
  182. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersDynamic.d.ts +2 -0
  183. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersDynamic.js +1 -0
  184. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersMobilePopup.d.ts +2 -0
  185. package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersMobilePopup.js +1 -0
  186. package/dist/modules/search-form/hotel/hotel-filters/RangeBlock.d.ts +1 -0
  187. package/dist/modules/search-form/hotel/hotel-filters/RangeBlock.js +1 -0
  188. package/dist/modules/search-form/hotel/hotel-filters/index.d.ts +1 -0
  189. package/dist/modules/search-form/hotel/hotel-filters/index.js +1 -0
  190. package/dist/modules/tabs/FlightsTab.d.ts +1 -0
  191. package/dist/modules/tabs/FlightsTab.js +1 -0
  192. package/dist/modules/tabs/HotelsTab.d.ts +1 -0
  193. package/dist/modules/tabs/HotelsTab.js +1 -0
  194. package/dist/store/index.d.ts +22 -0
  195. package/dist/store/index.js +19 -0
  196. package/dist/theme/colors.d.ts +56 -0
  197. package/dist/theme/colors.js +56 -0
  198. package/dist/theme/formStyles.d.ts +76 -0
  199. package/dist/theme/formStyles.js +78 -0
  200. package/dist/utils/applyWidgetColors.d.ts +1 -0
  201. package/dist/utils/applyWidgetColors.js +2 -0
  202. package/dist/utils/dateTime.d.ts +2 -0
  203. package/dist/utils/dateTime.js +23 -0
  204. package/dist/utils/fetchSSE.d.ts +23 -0
  205. package/dist/utils/fetchSSE.js +104 -0
  206. package/dist/utils/getToken.d.ts +3 -0
  207. package/dist/utils/getToken.js +12 -0
  208. package/package.json +64 -0
@@ -0,0 +1,318 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo, useCallback } from "react";
3
+ import { View, Text, TouchableOpacity, StyleSheet, } from "react-native";
4
+ import Svg, { Path } from "react-native-svg";
5
+ import { Modal } from "../../../../components/ui/Modal";
6
+ import { FilterSection } from "./FilterSection";
7
+ import { useCarFilters } from "./useCarFilters";
8
+ import { useWidget } from "../../../../context/WidgetContext";
9
+ import { Colors, Spacing, FontSize, BorderRadius } from "../../../../theme/colors";
10
+ const CURRENCY_SIGNS = {
11
+ USD: "$",
12
+ EUR: "\u20AC",
13
+ GBP: "\u00A3",
14
+ CHF: "\u20A3",
15
+ };
16
+ const CheckboxItem = ({ checked, label, onPress, description, }) => (_jsxs(TouchableOpacity, { style: styles.checkboxRow, onPress: onPress, activeOpacity: 0.7, children: [_jsx(View, { style: [styles.checkbox, checked && styles.checkboxChecked], children: checked ? _jsx(Text, { style: styles.checkmark, children: "\u2713" }) : null }), _jsxs(View, { style: styles.checkboxLabelContainer, children: [_jsx(Text, { style: styles.checkboxLabel, children: label }), description ? (_jsx(Text, { style: styles.checkboxDescription, children: description })) : null] })] }));
17
+ const SelectClearButtons = ({ onSelectAll, onClearAll, translations, }) => (_jsxs(View, { style: styles.selectClearRow, children: [_jsx(TouchableOpacity, { onPress: onSelectAll, activeOpacity: 0.7, children: _jsx(Text, { style: styles.selectAllText, children: translations["filter_select_all"] || "Select all" }) }), _jsx(TouchableOpacity, { onPress: onClearAll, activeOpacity: 0.7, children: _jsx(Text, { style: styles.clearAllText, children: translations["filter_remove_all"] || "Remove all" }) })] }));
18
+ export const CarFilters = ({ offers, onFilterChange, onFiltersApplied, translations, }) => {
19
+ const { currency } = useWidget();
20
+ const currencySign = CURRENCY_SIGNS[currency] || currency;
21
+ const { filters, setFilters, resetFilters, filteredOffers, UI_MIN_PRICE, UI_MAX_PRICE, handleRangeChange, toggle, } = useCarFilters(offers);
22
+ const [openSections, setOpenSections] = useState([]);
23
+ const [isModalOpen, setIsModalOpen] = useState(false);
24
+ const toggleSection = useCallback((key) => {
25
+ setOpenSections((prev) => prev.includes(key)
26
+ ? prev.filter((k) => k !== key)
27
+ : [...prev, key]);
28
+ }, []);
29
+ useEffect(() => {
30
+ if (offers.length > 0) {
31
+ onFiltersApplied?.();
32
+ }
33
+ }, [filters]);
34
+ useEffect(() => {
35
+ onFilterChange?.(filteredOffers);
36
+ }, [filteredOffers]);
37
+ const uniqueSuppliers = useMemo(() => {
38
+ const map = new Map();
39
+ (offers || []).forEach((o) => {
40
+ const id = o?.supplier?.id;
41
+ const name = o?.supplier?.name;
42
+ if (id != null && name)
43
+ map.set(String(id), String(name));
44
+ });
45
+ return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
46
+ }, [offers]);
47
+ const uniqueBrands = useMemo(() => {
48
+ const set = new Set();
49
+ (offers || []).forEach((o) => {
50
+ const rawBrand = String(o?.car?.name || "").split(" ")[0];
51
+ const brand = rawBrand.trim().replace(/\s+/g, " ");
52
+ if (brand)
53
+ set.add(brand);
54
+ });
55
+ return Array.from(set).sort((a, b) => a.localeCompare(b));
56
+ }, [offers]);
57
+ const includedOptionIds = useMemo(() => {
58
+ const map = new Map();
59
+ (offers || []).forEach((o) => {
60
+ (o?.included_options || []).forEach((opt) => {
61
+ if (opt?.id != null) {
62
+ map.set(String(opt.id), String(opt?.name || `Option ${opt.id}`));
63
+ }
64
+ });
65
+ });
66
+ return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1]));
67
+ }, [offers]);
68
+ useEffect(() => {
69
+ if (filters.suppliers.length === 0 && uniqueSuppliers.length > 0) {
70
+ setFilters((prev) => ({
71
+ ...prev,
72
+ suppliers: uniqueSuppliers.map(([id]) => Number(id)),
73
+ }));
74
+ }
75
+ if (filters.brands.length === 0 && uniqueBrands.length > 0) {
76
+ setFilters((prev) => ({
77
+ ...prev,
78
+ brands: uniqueBrands,
79
+ }));
80
+ }
81
+ }, [uniqueSuppliers, uniqueBrands]);
82
+ const normalizeKey = (name) => name
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9]+/g, "_")
85
+ .replace(/_+/g, "_")
86
+ .replace(/^_|_$/g, "");
87
+ const renderFiltersContent = () => (_jsxs(View, { style: styles.filtersContent, children: [_jsx(Text, { style: styles.filtersTitle, children: translations["filters_title"] || "Filters" }), _jsxs(View, { style: styles.priceSection, children: [_jsx(Text, { style: styles.sectionTitle, children: translations["filter_price"] || "Price" }), _jsxs(View, { style: styles.priceRange, children: [_jsxs(Text, { style: styles.priceText, children: [Math.round(filters.price.min), " ", currencySign] }), _jsx(Text, { style: styles.priceText, children: filters.price.max >= UI_MAX_PRICE
88
+ ? `${UI_MAX_PRICE}+ ${currencySign}`
89
+ : `${Math.round(filters.price.max)} ${currencySign}` })] }), _jsx(View, { style: styles.priceBar, children: _jsx(View, { style: [
90
+ styles.priceBarFill,
91
+ {
92
+ left: `${((filters.price.min - UI_MIN_PRICE) / (UI_MAX_PRICE - UI_MIN_PRICE)) * 100}%`,
93
+ right: `${100 - ((filters.price.max - UI_MIN_PRICE) / (UI_MAX_PRICE - UI_MIN_PRICE)) * 100}%`,
94
+ },
95
+ ] }) }), _jsxs(View, { style: styles.priceButtonsRow, children: [_jsx(TouchableOpacity, { style: styles.priceAdjustBtn, onPress: () => handleRangeChange("min", Math.max(UI_MIN_PRICE, filters.price.min - 100)), activeOpacity: 0.7, children: _jsx(Text, { style: styles.priceAdjustText, children: "- Min" }) }), _jsx(TouchableOpacity, { style: styles.priceAdjustBtn, onPress: () => handleRangeChange("min", Math.min(filters.price.max - 20, filters.price.min + 100)), activeOpacity: 0.7, children: _jsx(Text, { style: styles.priceAdjustText, children: "+ Min" }) }), _jsx(TouchableOpacity, { style: styles.priceAdjustBtn, onPress: () => handleRangeChange("max", Math.max(filters.price.min + 20, filters.price.max - 100)), activeOpacity: 0.7, children: _jsx(Text, { style: styles.priceAdjustText, children: "- Max" }) }), _jsx(TouchableOpacity, { style: styles.priceAdjustBtn, onPress: () => handleRangeChange("max", Math.min(UI_MAX_PRICE, filters.price.max + 100)), activeOpacity: 0.7, children: _jsx(Text, { style: styles.priceAdjustText, children: "+ Max" }) })] })] }), _jsx(FilterSection, { title: translations["car_filters_transmission"] || "Transmission", isOpen: openSections.includes("transmission"), onToggle: () => toggleSection("transmission"), children: ["automatic", "manual"].map((val) => (_jsx(CheckboxItem, { checked: filters.transmission.includes(val), label: val === "automatic"
96
+ ? translations["car_filters_transmission_auto"] || "Automatic"
97
+ : translations["car_filters_transmission_manual"] || "Manual", onPress: () => toggle("transmission", val) }, val))) }), _jsx(FilterSection, { title: translations["car_filters_seats"] || "Seats", isOpen: openSections.includes("seats"), onToggle: () => toggleSection("seats"), children: [4, 5, 6].map((seat) => (_jsx(CheckboxItem, { checked: filters.seats.includes(seat), label: seat === 6 ? "6+" : String(seat), onPress: () => toggle("seats", seat) }, seat))) }), _jsx(FilterSection, { title: translations["car_filters_options"] || "Options", isOpen: openSections.includes("includedOptions"), onToggle: () => toggleSection("includedOptions"), children: includedOptionIds.map(([id, name]) => {
98
+ const key = "car_option_" + normalizeKey(name);
99
+ const label = translations[key] || name;
100
+ return (_jsx(CheckboxItem, { checked: filters.includedOptions.includes(id), label: label, onPress: () => toggle("includedOptions", id) }, id));
101
+ }) }), _jsx(FilterSection, { title: translations["car_filters_fuel_policy"] || "Fuel policy", isOpen: openSections.includes("fuelPolicy"), onToggle: () => toggleSection("fuelPolicy"), children: [
102
+ {
103
+ value: "full_to_full",
104
+ title: translations["car_filters_fuel_policy_full_to_full_title"] ||
105
+ "Full to full",
106
+ description: translations["car_filters_fuel_policy_full_to_full_desc"] ||
107
+ "Pick up and return with a full tank.",
108
+ },
109
+ {
110
+ value: "same_to_same",
111
+ title: translations["car_filters_fuel_policy_same_to_same_title"] ||
112
+ "Same to same",
113
+ description: translations["car_filters_fuel_policy_same_to_same_desc"] ||
114
+ "Return the car with the same fuel level.",
115
+ },
116
+ ].map((item) => (_jsx(CheckboxItem, { checked: filters.fuelPolicy.includes(item.value), label: item.title, description: item.description, onPress: () => toggle("fuelPolicy", item.value) }, item.value))) }), _jsx(FilterSection, { title: translations["car_filters_mileage"] || "Mileage", isOpen: openSections.includes("mileage"), onToggle: () => toggleSection("mileage"), children: [
117
+ {
118
+ value: "unlimited",
119
+ title: translations["car_filters_mileage_unlimited_title"] ||
120
+ "Unlimited",
121
+ description: translations["car_filters_mileage_unlimited_desc"] ||
122
+ "Drive as much as you want.",
123
+ },
124
+ {
125
+ value: "limited",
126
+ title: translations["car_filters_mileage_limited_title"] || "Limited",
127
+ description: translations["car_filters_mileage_limited_desc"] ||
128
+ "There is a kilometer limit.",
129
+ },
130
+ ].map((item) => (_jsx(CheckboxItem, { checked: filters.mileage.includes(item.value), label: item.title, description: item.description, onPress: () => toggle("mileage", item.value) }, item.value))) }), _jsxs(FilterSection, { title: translations["car_filters_suppliers"] || "Suppliers", isOpen: openSections.includes("suppliers"), onToggle: () => toggleSection("suppliers"), children: [uniqueSuppliers.map(([id, name]) => (_jsx(CheckboxItem, { checked: filters.suppliers.includes(Number(id)), label: name, onPress: () => toggle("suppliers", Number(id)) }, id))), _jsx(SelectClearButtons, { onSelectAll: () => setFilters((prev) => ({
131
+ ...prev,
132
+ suppliers: uniqueSuppliers.map(([id]) => Number(id)),
133
+ })), onClearAll: () => setFilters((prev) => ({ ...prev, suppliers: [] })), translations: translations })] }), _jsxs(FilterSection, { title: translations["car_filters_brand"] || "Car brand", isOpen: openSections.includes("brands"), onToggle: () => toggleSection("brands"), children: [uniqueBrands.map((brand) => (_jsx(CheckboxItem, { checked: filters.brands.includes(brand), label: brand, onPress: () => toggle("brands", brand) }, brand))), _jsx(SelectClearButtons, { onSelectAll: () => setFilters((prev) => ({
134
+ ...prev,
135
+ brands: uniqueBrands,
136
+ })), onClearAll: () => setFilters((prev) => ({ ...prev, brands: [] })), translations: translations })] })] }));
137
+ return (_jsxs(_Fragment, { children: [_jsxs(TouchableOpacity, { style: styles.openButton, onPress: () => setIsModalOpen(true), activeOpacity: 0.8, children: [_jsxs(Svg, { width: 24, height: 20, viewBox: "0 0 24 20", fill: "none", children: [_jsx(Path, { d: "M18.5 4.1665H5.5C4.67157 4.1665 4 4.72615 4 5.4165C4 6.10686 4.67157 6.6665 5.5 6.6665H18.5C19.3284 6.6665 20 6.10686 20 5.4165C20 4.72615 19.3284 4.1665 18.5 4.1665Z", fill: Colors.primary }), _jsx(Path, { opacity: 0.3, fillRule: "evenodd", clipRule: "evenodd", d: "M7.5 9.1665H16.5C17.3284 9.1665 18 9.72615 18 10.4165C18 11.1069 17.3284 11.6665 16.5 11.6665H7.5C6.67157 11.6665 6 11.1069 6 10.4165C6 9.72615 6.67157 9.1665 7.5 9.1665ZM10.5 14.1665H13.5C14.3284 14.1665 15 14.7261 15 15.4165C15 16.1069 14.3284 16.6665 13.5 16.6665H10.5C9.67157 16.6665 9 16.1069 9 15.4165C9 14.7261 9.67157 14.1665 10.5 14.1665Z", fill: Colors.primary })] }), _jsx(Text, { style: styles.openButtonText, children: translations["filters_title"] || "Filters" })] }), _jsx(Modal, { visible: isModalOpen, onClose: () => setIsModalOpen(false), title: translations["filters_title"] || "Filters", fullScreen: true, children: _jsxs(View, { style: styles.modalInner, children: [renderFiltersContent(), _jsxs(View, { style: styles.footerButtons, children: [_jsx(TouchableOpacity, { style: styles.resetButton, onPress: () => {
138
+ resetFilters();
139
+ }, activeOpacity: 0.7, children: _jsx(Text, { style: styles.resetButtonText, children: translations["filter_reset_btn"] || "Reset" }) }), _jsx(TouchableOpacity, { style: styles.applyButton, onPress: () => {
140
+ onFiltersApplied?.();
141
+ setIsModalOpen(false);
142
+ }, activeOpacity: 0.8, children: _jsx(Text, { style: styles.applyButtonText, children: translations["filter_apply_btn"] || "Apply" }) })] })] }) })] }));
143
+ };
144
+ const styles = StyleSheet.create({
145
+ openButton: {
146
+ flexDirection: "row",
147
+ alignItems: "center",
148
+ gap: Spacing.sm,
149
+ backgroundColor: Colors.surface,
150
+ borderRadius: BorderRadius.md,
151
+ paddingVertical: Spacing.md,
152
+ paddingHorizontal: Spacing.lg,
153
+ marginHorizontal: Spacing.lg,
154
+ marginBottom: Spacing.md,
155
+ borderWidth: 1,
156
+ borderColor: Colors.border,
157
+ shadowColor: Colors.black,
158
+ shadowOffset: { width: 0, height: 1 },
159
+ shadowOpacity: 0.05,
160
+ shadowRadius: 2,
161
+ elevation: 1,
162
+ },
163
+ openButtonText: {
164
+ fontSize: FontSize.md,
165
+ color: Colors.text,
166
+ fontWeight: "500",
167
+ },
168
+ modalInner: {
169
+ flex: 1,
170
+ },
171
+ filtersContent: {
172
+ gap: Spacing.xs,
173
+ },
174
+ filtersTitle: {
175
+ fontSize: FontSize.xl,
176
+ fontWeight: "700",
177
+ color: Colors.text,
178
+ marginBottom: Spacing.md,
179
+ },
180
+ priceSection: {
181
+ paddingBottom: Spacing.md,
182
+ borderBottomWidth: StyleSheet.hairlineWidth,
183
+ borderBottomColor: Colors.borderLight,
184
+ },
185
+ sectionTitle: {
186
+ fontSize: FontSize.md,
187
+ fontWeight: "600",
188
+ color: Colors.text,
189
+ marginBottom: Spacing.sm,
190
+ },
191
+ priceRange: {
192
+ flexDirection: "row",
193
+ justifyContent: "space-between",
194
+ marginBottom: Spacing.sm,
195
+ },
196
+ priceText: {
197
+ fontSize: FontSize.sm,
198
+ color: Colors.textSecondary,
199
+ fontWeight: "500",
200
+ },
201
+ priceBar: {
202
+ height: 4,
203
+ backgroundColor: Colors.borderLight,
204
+ borderRadius: 2,
205
+ marginBottom: Spacing.md,
206
+ position: "relative",
207
+ },
208
+ priceBarFill: {
209
+ position: "absolute",
210
+ top: 0,
211
+ bottom: 0,
212
+ backgroundColor: Colors.primary,
213
+ borderRadius: 2,
214
+ },
215
+ priceButtonsRow: {
216
+ flexDirection: "row",
217
+ gap: Spacing.sm,
218
+ flexWrap: "wrap",
219
+ },
220
+ priceAdjustBtn: {
221
+ paddingVertical: Spacing.xs,
222
+ paddingHorizontal: Spacing.md,
223
+ borderRadius: BorderRadius.sm,
224
+ borderWidth: 1,
225
+ borderColor: Colors.border,
226
+ backgroundColor: Colors.surfaceSecondary,
227
+ },
228
+ priceAdjustText: {
229
+ fontSize: FontSize.xs,
230
+ color: Colors.textSecondary,
231
+ fontWeight: "500",
232
+ },
233
+ checkboxRow: {
234
+ flexDirection: "row",
235
+ alignItems: "flex-start",
236
+ gap: Spacing.sm,
237
+ paddingVertical: Spacing.xs,
238
+ },
239
+ checkbox: {
240
+ width: 22,
241
+ height: 22,
242
+ borderRadius: BorderRadius.sm,
243
+ borderWidth: 2,
244
+ borderColor: Colors.border,
245
+ alignItems: "center",
246
+ justifyContent: "center",
247
+ marginTop: 1,
248
+ },
249
+ checkboxChecked: {
250
+ backgroundColor: Colors.primary,
251
+ borderColor: Colors.primary,
252
+ },
253
+ checkmark: {
254
+ color: Colors.textOnPrimary,
255
+ fontSize: FontSize.xs,
256
+ fontWeight: "700",
257
+ },
258
+ checkboxLabelContainer: {
259
+ flex: 1,
260
+ },
261
+ checkboxLabel: {
262
+ fontSize: FontSize.md,
263
+ color: Colors.text,
264
+ },
265
+ checkboxDescription: {
266
+ fontSize: FontSize.xs,
267
+ color: Colors.textTertiary,
268
+ marginTop: 2,
269
+ },
270
+ selectClearRow: {
271
+ flexDirection: "row",
272
+ gap: Spacing.lg,
273
+ marginTop: Spacing.xs,
274
+ },
275
+ selectAllText: {
276
+ fontSize: FontSize.sm,
277
+ color: Colors.primary,
278
+ fontWeight: "500",
279
+ },
280
+ clearAllText: {
281
+ fontSize: FontSize.sm,
282
+ color: Colors.textTertiary,
283
+ fontWeight: "500",
284
+ },
285
+ footerButtons: {
286
+ flexDirection: "row",
287
+ gap: Spacing.md,
288
+ marginTop: Spacing.xl,
289
+ paddingTop: Spacing.md,
290
+ borderTopWidth: 1,
291
+ borderTopColor: Colors.borderLight,
292
+ },
293
+ resetButton: {
294
+ flex: 1,
295
+ paddingVertical: Spacing.md,
296
+ borderRadius: BorderRadius.md,
297
+ borderWidth: 1,
298
+ borderColor: Colors.border,
299
+ alignItems: "center",
300
+ },
301
+ resetButtonText: {
302
+ fontSize: FontSize.md,
303
+ color: Colors.textSecondary,
304
+ fontWeight: "600",
305
+ },
306
+ applyButton: {
307
+ flex: 1,
308
+ paddingVertical: Spacing.md,
309
+ borderRadius: BorderRadius.md,
310
+ backgroundColor: Colors.primary,
311
+ alignItems: "center",
312
+ },
313
+ applyButtonText: {
314
+ fontSize: FontSize.md,
315
+ color: Colors.textOnPrimary,
316
+ fontWeight: "600",
317
+ },
318
+ });
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ export type FilterSectionProps = {
3
+ id?: string;
4
+ title: string;
5
+ isOpen: boolean;
6
+ onToggle: () => void;
7
+ children: React.ReactNode;
8
+ };
9
+ export declare const FilterSection: React.FC<FilterSectionProps>;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View, Text, TouchableOpacity, StyleSheet, } from "react-native";
3
+ import Svg, { Path } from "react-native-svg";
4
+ import { Colors, Spacing, FontSize } from "../../../../theme/colors";
5
+ const ChevronIcon = ({ rotated }) => (_jsx(Svg, { width: 20, height: 20, viewBox: "0 0 24 24", fill: Colors.textSecondary, style: rotated ? { transform: [{ rotate: "-90deg" }] } : undefined, children: _jsx(Path, { d: "M16 19.5 14.5 21 6 12.5v-1L14.5 3 16 4.5 8.5 12l7.5 7.5Z" }) }));
6
+ export const FilterSection = ({ title, isOpen, onToggle, children, }) => (_jsxs(View, { style: styles.container, children: [_jsxs(TouchableOpacity, { style: styles.header, onPress: onToggle, activeOpacity: 0.7, children: [_jsx(Text, { style: styles.title, children: title }), _jsx(ChevronIcon, { rotated: isOpen })] }), isOpen ? _jsx(View, { style: styles.content, children: children }) : null] }));
7
+ const styles = StyleSheet.create({
8
+ container: {
9
+ borderBottomWidth: StyleSheet.hairlineWidth,
10
+ borderBottomColor: Colors.borderLight,
11
+ },
12
+ header: {
13
+ flexDirection: "row",
14
+ alignItems: "center",
15
+ justifyContent: "space-between",
16
+ paddingVertical: Spacing.md,
17
+ paddingHorizontal: Spacing.sm,
18
+ },
19
+ title: {
20
+ fontSize: FontSize.md,
21
+ fontWeight: "600",
22
+ color: Colors.text,
23
+ flex: 1,
24
+ },
25
+ content: {
26
+ paddingHorizontal: Spacing.sm,
27
+ paddingBottom: Spacing.md,
28
+ gap: Spacing.sm,
29
+ },
30
+ });
@@ -0,0 +1,6 @@
1
+ export { CarFilters } from "./CarFilters";
2
+ export type { CarFiltersProps } from "./CarFilters";
3
+ export { FilterSection } from "./FilterSection";
4
+ export type { FilterSectionProps } from "./FilterSection";
5
+ export { useCarFilters } from "./useCarFilters";
6
+ export type { CarFiltersState } from "./useCarFilters";
@@ -0,0 +1,3 @@
1
+ export { CarFilters } from "./CarFilters";
2
+ export { FilterSection } from "./FilterSection";
3
+ export { useCarFilters } from "./useCarFilters";
@@ -0,0 +1,24 @@
1
+ export type CarFiltersState = {
2
+ price: {
3
+ min: number;
4
+ max: number;
5
+ };
6
+ transmission: string[];
7
+ seats: number[];
8
+ fuelPolicy: string[];
9
+ mileage: string[];
10
+ suppliers: number[];
11
+ brands: string[];
12
+ includedOptions: string[];
13
+ category: string[];
14
+ };
15
+ export declare const useCarFilters: (offers: any[]) => {
16
+ filters: CarFiltersState;
17
+ setFilters: import("react").Dispatch<import("react").SetStateAction<CarFiltersState>>;
18
+ resetFilters: () => void;
19
+ filteredOffers: any[];
20
+ UI_MIN_PRICE: number;
21
+ UI_MAX_PRICE: number;
22
+ handleRangeChange: (type: "min" | "max", value: number) => void;
23
+ toggle: (name: keyof CarFiltersState, value: string | number) => void;
24
+ };
@@ -0,0 +1,121 @@
1
+ import { useEffect, useState } from "react";
2
+ const UI_MIN_PRICE = 0;
3
+ const UI_MAX_PRICE = 4000;
4
+ const DEFAULT_STATE = {
5
+ price: { min: UI_MIN_PRICE, max: UI_MAX_PRICE },
6
+ transmission: [],
7
+ seats: [],
8
+ fuelPolicy: [],
9
+ mileage: [],
10
+ suppliers: [],
11
+ brands: [],
12
+ includedOptions: [],
13
+ category: [],
14
+ };
15
+ export const useCarFilters = (offers) => {
16
+ const [filters, setFilters] = useState(DEFAULT_STATE);
17
+ const [filteredOffers, setFilteredOffers] = useState(offers);
18
+ useEffect(() => {
19
+ const timer = setTimeout(() => {
20
+ let filtered = [...offers];
21
+ const maxCap = filters.price.max >= UI_MAX_PRICE
22
+ ? Infinity
23
+ : filters.price.max;
24
+ const minCap = Math.max(UI_MIN_PRICE, filters.price.min);
25
+ filtered = filtered.filter((o) => {
26
+ const total = Number(o?.price?.total) || 0;
27
+ return total >= minCap && total <= maxCap;
28
+ });
29
+ if (filters.transmission.length) {
30
+ filtered = filtered.filter((o) => filters.transmission.includes(o?.car?.transmission));
31
+ }
32
+ if (filters.seats.length) {
33
+ filtered = filtered.filter((o) => {
34
+ const s = Number(o?.car?.seats) || 0;
35
+ return filters.seats.some((want) => want >= 6 ? s >= 6 : s === want);
36
+ });
37
+ }
38
+ if (filters.fuelPolicy.length) {
39
+ filtered = filtered.filter((o) => filters.fuelPolicy.includes(o?.car?.fuel));
40
+ }
41
+ if (filters.mileage.length) {
42
+ filtered = filtered.filter((o) => {
43
+ const type = o?.mileage?.unlimited ? "unlimited" : "limited";
44
+ return filters.mileage.includes(type);
45
+ });
46
+ }
47
+ if (filters.suppliers.length) {
48
+ filtered = filtered.filter((o) => filters.suppliers.includes(Number(o?.supplier?.id)));
49
+ }
50
+ if (filters.brands.length) {
51
+ filtered = filtered.filter((o) => {
52
+ const brand = String(o?.car?.name || "").split(" ")[0].trim();
53
+ return filters.brands.includes(brand);
54
+ });
55
+ }
56
+ if (filters.category.length) {
57
+ filtered = filtered.filter((o) => {
58
+ const cat = String(o?.car?.category || "").toLowerCase();
59
+ return filters.category.includes(cat);
60
+ });
61
+ }
62
+ if (filters.includedOptions.length) {
63
+ filtered = filtered.filter((o) => {
64
+ const includedIds = (o?.included_options || []).map((opt) => String(opt?.id));
65
+ return filters.includedOptions.every((id) => includedIds.includes(id));
66
+ });
67
+ }
68
+ console.log("🔍 Активные фильтры:", filters);
69
+ console.log("✅ Найдено офферов:", filtered.length);
70
+ setFilteredOffers(filtered);
71
+ }, 120);
72
+ return () => clearTimeout(timer);
73
+ }, [filters, offers]);
74
+ const resetFilters = () => setFilters(DEFAULT_STATE);
75
+ const SLIDER_GAP = 20;
76
+ const handleRangeChange = (type, value) => {
77
+ setFilters(prev => {
78
+ let nextMin = prev.price.min;
79
+ let nextMax = prev.price.max;
80
+ if (type === "min") {
81
+ nextMin = value;
82
+ if (nextMin > nextMax - SLIDER_GAP) {
83
+ nextMin = nextMax - SLIDER_GAP;
84
+ }
85
+ nextMin = Math.max(UI_MIN_PRICE, nextMin);
86
+ }
87
+ if (type === "max") {
88
+ nextMax = value;
89
+ if (nextMax < nextMin + SLIDER_GAP) {
90
+ nextMax = nextMin + SLIDER_GAP;
91
+ }
92
+ nextMax = Math.min(UI_MAX_PRICE, nextMax);
93
+ }
94
+ return {
95
+ ...prev,
96
+ price: { min: nextMin, max: nextMax }
97
+ };
98
+ });
99
+ };
100
+ const toggle = (name, value) => {
101
+ setFilters((prev) => {
102
+ const arr = prev[name];
103
+ return {
104
+ ...prev,
105
+ [name]: arr.includes(value)
106
+ ? arr.filter((v) => v !== value)
107
+ : [...arr, value],
108
+ };
109
+ });
110
+ };
111
+ return {
112
+ filters,
113
+ setFilters,
114
+ resetFilters,
115
+ filteredOffers,
116
+ UI_MIN_PRICE,
117
+ UI_MAX_PRICE,
118
+ handleRangeChange,
119
+ toggle,
120
+ };
121
+ };
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ type AirlinesSelectorProps = {
3
+ value: string[];
4
+ onChange: (val: string[]) => void;
5
+ translations: Record<string, string>;
6
+ };
7
+ declare const AirlinesSelector: React.FC<AirlinesSelectorProps>;
8
+ export default AirlinesSelector;