@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.
- package/LICENSE +18 -0
- package/dist/App.d.ts +10 -0
- package/dist/App.js +192 -0
- package/dist/TravelHubWidget.d.ts +12 -0
- package/dist/TravelHubWidget.js +58 -0
- package/dist/components/ErrorBoundary.d.ts +18 -0
- package/dist/components/ErrorBoundary.js +63 -0
- package/dist/components/ui/Modal.d.ts +11 -0
- package/dist/components/ui/Modal.js +66 -0
- package/dist/components/ui/Skeleton.d.ts +8 -0
- package/dist/components/ui/Skeleton.js +33 -0
- package/dist/components/ui/badge.d.ts +11 -0
- package/dist/components/ui/badge.js +54 -0
- package/dist/components/ui/button.d.ts +14 -0
- package/dist/components/ui/button.js +101 -0
- package/dist/components/ui/card.d.ts +16 -0
- package/dist/components/ui/card.js +51 -0
- package/dist/components/ui/input.d.ts +8 -0
- package/dist/components/ui/input.js +61 -0
- package/dist/components/ui/label.d.ts +8 -0
- package/dist/components/ui/label.js +17 -0
- package/dist/context/WidgetContext.d.ts +29 -0
- package/dist/context/WidgetContext.js +64 -0
- package/dist/icons/CarIcon.d.ts +4 -0
- package/dist/icons/CarIcon.js +3 -0
- package/dist/icons/CloseIcon.d.ts +7 -0
- package/dist/icons/CloseIcon.js +3 -0
- package/dist/icons/FlightIcon.d.ts +4 -0
- package/dist/icons/FlightIcon.js +3 -0
- package/dist/icons/HotelIcon.d.ts +4 -0
- package/dist/icons/HotelIcon.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +3 -0
- package/dist/locales/widgetTranslations.d.ts +1 -0
- package/dist/locales/widgetTranslations.js +732 -0
- package/dist/modules/hooks/useAirportSearch.d.ts +21 -0
- package/dist/modules/hooks/useAirportSearch.js +48 -0
- package/dist/modules/hooks/useCarOffers.d.ts +7 -0
- package/dist/modules/hooks/useCarOffers.js +47 -0
- package/dist/modules/hooks/useCountriesByCodes.d.ts +4 -0
- package/dist/modules/hooks/useCountriesByCodes.js +60 -0
- package/dist/modules/hooks/useCountriesSearch.d.ts +11 -0
- package/dist/modules/hooks/useCountriesSearch.js +55 -0
- package/dist/modules/hooks/useDefaultHotelLocation.d.ts +7 -0
- package/dist/modules/hooks/useDefaultHotelLocation.js +44 -0
- package/dist/modules/hooks/useDetectNationality.d.ts +1 -0
- package/dist/modules/hooks/useDetectNationality.js +20 -0
- package/dist/modules/hooks/useFlightSearch.d.ts +20 -0
- package/dist/modules/hooks/useFlightSearch.js +243 -0
- package/dist/modules/hooks/useHotelDetails.d.ts +7 -0
- package/dist/modules/hooks/useHotelDetails.js +80 -0
- package/dist/modules/hooks/useHotelLocations.d.ts +5 -0
- package/dist/modules/hooks/useHotelLocations.js +42 -0
- package/dist/modules/hooks/useHotelMapData.d.ts +12 -0
- package/dist/modules/hooks/useHotelMapData.js +78 -0
- package/dist/modules/hooks/useHotelOffers.d.ts +11 -0
- package/dist/modules/hooks/useHotelOffers.js +122 -0
- package/dist/modules/hooks/useHotelSearchSSE.d.ts +18 -0
- package/dist/modules/hooks/useHotelSearchSSE.js +411 -0
- package/dist/modules/hooks/useSearchExpiration.d.ts +6 -0
- package/dist/modules/hooks/useSearchExpiration.js +35 -0
- package/dist/modules/hooks/useTranslationsHub.d.ts +5 -0
- package/dist/modules/hooks/useTranslationsHub.js +48 -0
- package/dist/modules/navbar/Navbar.d.ts +8 -0
- package/dist/modules/navbar/Navbar.js +69 -0
- package/dist/modules/search-form/AgeSelector.d.ts +9 -0
- package/dist/modules/search-form/AgeSelector.js +109 -0
- package/dist/modules/search-form/CustomSelect.d.ts +14 -0
- package/dist/modules/search-form/CustomSelect.js +94 -0
- package/dist/modules/search-form/FiltersSkeleton.d.ts +6 -0
- package/dist/modules/search-form/FiltersSkeleton.js +35 -0
- package/dist/modules/search-form/NationalityInput.d.ts +18 -0
- package/dist/modules/search-form/NationalityInput.js +172 -0
- package/dist/modules/search-form/Pagination.d.ts +8 -0
- package/dist/modules/search-form/Pagination.js +84 -0
- package/dist/modules/search-form/SearchExpiredPopup.d.ts +10 -0
- package/dist/modules/search-form/SearchExpiredPopup.js +91 -0
- package/dist/modules/search-form/SkeletonWidget.d.ts +2 -0
- package/dist/modules/search-form/SkeletonWidget.js +33 -0
- package/dist/modules/search-form/TimePicker.d.ts +9 -0
- package/dist/modules/search-form/TimePicker.js +182 -0
- package/dist/modules/search-form/cars/CarEmptyState.d.ts +6 -0
- package/dist/modules/search-form/cars/CarEmptyState.js +42 -0
- package/dist/modules/search-form/cars/CarFiltersSkeleton.d.ts +6 -0
- package/dist/modules/search-form/cars/CarFiltersSkeleton.js +35 -0
- package/dist/modules/search-form/cars/CarOffersList.d.ts +43 -0
- package/dist/modules/search-form/cars/CarOffersList.js +214 -0
- package/dist/modules/search-form/cars/CarResults.d.ts +25 -0
- package/dist/modules/search-form/cars/CarResults.js +116 -0
- package/dist/modules/search-form/cars/CarSearchForm.d.ts +19 -0
- package/dist/modules/search-form/cars/CarSearchForm.js +173 -0
- package/dist/modules/search-form/cars/CarSkeleton.d.ts +6 -0
- package/dist/modules/search-form/cars/CarSkeleton.js +46 -0
- package/dist/modules/search-form/cars/CarTopOffersSlider.d.ts +20 -0
- package/dist/modules/search-form/cars/CarTopOffersSlider.js +100 -0
- package/dist/modules/search-form/cars/car-filters/CarFilters.d.ts +8 -0
- package/dist/modules/search-form/cars/car-filters/CarFilters.js +318 -0
- package/dist/modules/search-form/cars/car-filters/FilterSection.d.ts +9 -0
- package/dist/modules/search-form/cars/car-filters/FilterSection.js +30 -0
- package/dist/modules/search-form/cars/car-filters/index.d.ts +6 -0
- package/dist/modules/search-form/cars/car-filters/index.js +3 -0
- package/dist/modules/search-form/cars/car-filters/useCarFilters.d.ts +24 -0
- package/dist/modules/search-form/cars/car-filters/useCarFilters.js +121 -0
- package/dist/modules/search-form/flight/AirlinesSelector.d.ts +8 -0
- package/dist/modules/search-form/flight/AirlinesSelector.js +235 -0
- package/dist/modules/search-form/flight/CabinClassSelector.d.ts +9 -0
- package/dist/modules/search-form/flight/CabinClassSelector.js +63 -0
- package/dist/modules/search-form/flight/DatePicker.d.ts +12 -0
- package/dist/modules/search-form/flight/DatePicker.js +536 -0
- package/dist/modules/search-form/flight/FlightSearchForm.d.ts +18 -0
- package/dist/modules/search-form/flight/FlightSearchForm.js +147 -0
- package/dist/modules/search-form/flight/LocationInput.d.ts +11 -0
- package/dist/modules/search-form/flight/LocationInput.js +294 -0
- package/dist/modules/search-form/flight/PassengersPicker.d.ts +16 -0
- package/dist/modules/search-form/flight/PassengersPicker.js +203 -0
- package/dist/modules/search-form/flight/SwitchLocationBtn.d.ts +9 -0
- package/dist/modules/search-form/flight/SwitchLocationBtn.js +48 -0
- package/dist/modules/search-form/flight/flight-filters/FiltersContent.d.ts +21 -0
- package/dist/modules/search-form/flight/flight-filters/FiltersContent.js +145 -0
- package/dist/modules/search-form/flight/flight-filters/FlightFiltersDynamic.d.ts +11 -0
- package/dist/modules/search-form/flight/flight-filters/FlightFiltersDynamic.js +213 -0
- package/dist/modules/search-form/flight/flight-filters/RangeBlock.d.ts +22 -0
- package/dist/modules/search-form/flight/flight-filters/RangeBlock.js +164 -0
- package/dist/modules/search-form/flight/flight-filters/index.d.ts +1 -0
- package/dist/modules/search-form/flight/flight-filters/index.js +1 -0
- package/dist/modules/search-form/flight/flight-results/FlightAdditionalInfo.d.ts +16 -0
- package/dist/modules/search-form/flight/flight-results/FlightAdditionalInfo.js +229 -0
- package/dist/modules/search-form/flight/flight-results/FlightBaggageToggle.d.ts +15 -0
- package/dist/modules/search-form/flight/flight-results/FlightBaggageToggle.js +110 -0
- package/dist/modules/search-form/flight/flight-results/FlightCard.d.ts +14 -0
- package/dist/modules/search-form/flight/flight-results/FlightCard.js +436 -0
- package/dist/modules/search-form/flight/flight-results/FlightInfo.d.ts +12 -0
- package/dist/modules/search-form/flight/flight-results/FlightInfo.js +48 -0
- package/dist/modules/search-form/flight/flight-results/FlightPriceBlock.d.ts +11 -0
- package/dist/modules/search-form/flight/flight-results/FlightPriceBlock.js +36 -0
- package/dist/modules/search-form/flight/flight-results/FlightResults.d.ts +22 -0
- package/dist/modules/search-form/flight/flight-results/FlightResults.js +109 -0
- package/dist/modules/search-form/flight/flight-results/FlightSegments.d.ts +40 -0
- package/dist/modules/search-form/flight/flight-results/FlightSegments.js +162 -0
- package/dist/modules/search-form/flight/flight-results/FlightsSkeleton.d.ts +6 -0
- package/dist/modules/search-form/flight/flight-results/FlightsSkeleton.js +52 -0
- package/dist/modules/search-form/flight/flight-results/ProvidersLoader.d.ts +9 -0
- package/dist/modules/search-form/flight/flight-results/ProvidersLoader.js +242 -0
- package/dist/modules/search-form/hotel/DatePicker.d.ts +1 -0
- package/dist/modules/search-form/hotel/DatePicker.js +1 -0
- package/dist/modules/search-form/hotel/GuestSelector.d.ts +13 -0
- package/dist/modules/search-form/hotel/GuestSelector.js +272 -0
- package/dist/modules/search-form/hotel/GuestSelectorMobilePopup.d.ts +2 -0
- package/dist/modules/search-form/hotel/GuestSelectorMobilePopup.js +1 -0
- package/dist/modules/search-form/hotel/HotelLocation.d.ts +17 -0
- package/dist/modules/search-form/hotel/HotelLocation.js +178 -0
- package/dist/modules/search-form/hotel/HotelLocationMobilePopup.d.ts +2 -0
- package/dist/modules/search-form/hotel/HotelLocationMobilePopup.js +1 -0
- package/dist/modules/search-form/hotel/HotelResults.d.ts +21 -0
- package/dist/modules/search-form/hotel/HotelResults.js +333 -0
- package/dist/modules/search-form/hotel/HotelSearchForm.d.ts +13 -0
- package/dist/modules/search-form/hotel/HotelSearchForm.js +130 -0
- package/dist/modules/search-form/hotel/HotelsSkeleton.d.ts +6 -0
- package/dist/modules/search-form/hotel/HotelsSkeleton.js +40 -0
- package/dist/modules/search-form/hotel/PassengersPicker.d.ts +2 -0
- package/dist/modules/search-form/hotel/PassengersPicker.js +1 -0
- package/dist/modules/search-form/hotel/components/HotelCard.d.ts +11 -0
- package/dist/modules/search-form/hotel/components/HotelCard.js +180 -0
- package/dist/modules/search-form/hotel/components/HotelEmptyState.d.ts +6 -0
- package/dist/modules/search-form/hotel/components/HotelEmptyState.js +34 -0
- package/dist/modules/search-form/hotel/components/HotelGallery.d.ts +6 -0
- package/dist/modules/search-form/hotel/components/HotelGallery.js +77 -0
- package/dist/modules/search-form/hotel/components/HotelList.d.ts +9 -0
- package/dist/modules/search-form/hotel/components/HotelList.js +12 -0
- package/dist/modules/search-form/hotel/components/HotelMap.d.ts +26 -0
- package/dist/modules/search-form/hotel/components/HotelMap.js +707 -0
- package/dist/modules/search-form/hotel/components/HotelPin.d.ts +7 -0
- package/dist/modules/search-form/hotel/components/HotelPin.js +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFilters.d.ts +29 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFilters.js +339 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersBar.d.ts +3 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersBar.js +2 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersContent.d.ts +2 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersContent.js +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersDynamic.d.ts +2 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersDynamic.js +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersMobilePopup.d.ts +2 -0
- package/dist/modules/search-form/hotel/hotel-filters/HotelFiltersMobilePopup.js +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/RangeBlock.d.ts +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/RangeBlock.js +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/index.d.ts +1 -0
- package/dist/modules/search-form/hotel/hotel-filters/index.js +1 -0
- package/dist/modules/tabs/FlightsTab.d.ts +1 -0
- package/dist/modules/tabs/FlightsTab.js +1 -0
- package/dist/modules/tabs/HotelsTab.d.ts +1 -0
- package/dist/modules/tabs/HotelsTab.js +1 -0
- package/dist/store/index.d.ts +22 -0
- package/dist/store/index.js +19 -0
- package/dist/theme/colors.d.ts +56 -0
- package/dist/theme/colors.js +56 -0
- package/dist/theme/formStyles.d.ts +76 -0
- package/dist/theme/formStyles.js +78 -0
- package/dist/utils/applyWidgetColors.d.ts +1 -0
- package/dist/utils/applyWidgetColors.js +2 -0
- package/dist/utils/dateTime.d.ts +2 -0
- package/dist/utils/dateTime.js +23 -0
- package/dist/utils/fetchSSE.d.ts +23 -0
- package/dist/utils/fetchSSE.js +104 -0
- package/dist/utils/getToken.d.ts +3 -0
- package/dist/utils/getToken.js +12 -0
- 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,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,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
|
+
};
|