@abcagency/hc-ui-components 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/dist/globals.css +3 -0
- package/dist/index.js +4644 -0
- package/dist/output.css +784 -0
- package/dist/services/globals.css +3 -0
- package/dist/services/listingService.js +606 -0
- package/package.json +38 -0
- package/postcss.config.js +15 -0
- package/rollup.config.js +67 -0
- package/src/apis/hcApi.js +68 -0
- package/src/clientToken.js +9 -0
- package/src/components/layout/footer.js +34 -0
- package/src/components/layout/header.js +23 -0
- package/src/components/layout/layout.js +36 -0
- package/src/components/modules/accordions/MapAccordionItem.js +69 -0
- package/src/components/modules/accordions/default.js +173 -0
- package/src/components/modules/accordions/filterItem.js +53 -0
- package/src/components/modules/accordions/filters.js +44 -0
- package/src/components/modules/animations/slidein.js +41 -0
- package/src/components/modules/buttons/button-group-apply.js +75 -0
- package/src/components/modules/buttons/commute-pill.js +21 -0
- package/src/components/modules/buttons/default.js +196 -0
- package/src/components/modules/buttons/items-pill.js +31 -0
- package/src/components/modules/buttons/pill-wrapper.js +26 -0
- package/src/components/modules/buttons/show-all-button.js +20 -0
- package/src/components/modules/cards/default.js +168 -0
- package/src/components/modules/cards/filter.js +55 -0
- package/src/components/modules/dialogs/apply-dialog.js +47 -0
- package/src/components/modules/filter/commute.js +149 -0
- package/src/components/modules/filter/index.js +86 -0
- package/src/components/modules/filter/item.js +77 -0
- package/src/components/modules/filter/location.js +69 -0
- package/src/components/modules/filter/points-of-interest.js +43 -0
- package/src/components/modules/filter/radio-item.js +51 -0
- package/src/components/modules/filter/search.js +89 -0
- package/src/components/modules/filter/search.js.rej +9 -0
- package/src/components/modules/filter/sort.js +83 -0
- package/src/components/modules/form.js +362 -0
- package/src/components/modules/grid.js +75 -0
- package/src/components/modules/icon.js +33 -0
- package/src/components/modules/jobListing/listing-details.js +87 -0
- package/src/components/modules/jumbotron.js +81 -0
- package/src/components/modules/maps/info-window-card.js +17 -0
- package/src/components/modules/maps/info-window-content.js +60 -0
- package/src/components/modules/maps/list/field-mapper.js +113 -0
- package/src/components/modules/maps/list/header-item.js +90 -0
- package/src/components/modules/maps/list/header.js +46 -0
- package/src/components/modules/maps/list/index.js +104 -0
- package/src/components/modules/maps/list/item-expand-card/index.js +21 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-contact-nav.js +48 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-details.js +67 -0
- package/src/components/modules/maps/list/item-expand-card/recruiter-headshot.js +22 -0
- package/src/components/modules/maps/list/list-item/index.js +133 -0
- package/src/components/modules/maps/map-list.js +73 -0
- package/src/components/modules/maps/map-marker.js +84 -0
- package/src/components/modules/maps/map.js +218 -0
- package/src/components/modules/maps/place-marker.js +41 -0
- package/src/components/modules/maps/tabs.js +79 -0
- package/src/components/modules/navigation/nav-link.js +65 -0
- package/src/components/modules/navigation/navbar.js +109 -0
- package/src/components/modules/navigation/skip-link.js +21 -0
- package/src/components/modules/navigation/social.js +29 -0
- package/src/components/modules/sections/default.js +59 -0
- package/src/components/modules/sections/sectionContext.js +4 -0
- package/src/components/modules/video-player.js +126 -0
- package/src/constants/placeTypes.js +8 -0
- package/src/contexts/mapContext.js +116 -0
- package/src/contexts/mapListContext.js +212 -0
- package/src/contexts/placesContext.js +98 -0
- package/src/hooks/useClickOutside.js +16 -0
- package/src/hooks/useEventListener.js +25 -0
- package/src/hooks/useEventTracker.js +19 -0
- package/src/hooks/useList.js +102 -0
- package/src/hooks/useRefScrollProgress.js +24 -0
- package/src/hooks/useScript.js +63 -0
- package/src/hooks/useScrollDirection.js +39 -0
- package/src/hooks/useSectionTracker.js +95 -0
- package/src/hooks/useUserAgent.js +43 -0
- package/src/hooks/useWindowSize.js +28 -0
- package/src/index.css +25 -0
- package/src/index.js +116 -0
- package/src/services/configService.js +16 -0
- package/src/services/googlePlacesNearbyService.js +33 -0
- package/src/services/listingAggregatorService.js +42 -0
- package/src/services/listingEntityService.js +14 -0
- package/src/services/listingService.js +28 -0
- package/src/services/recruiterService.js +17 -0
- package/src/styles/fonts.js +0 -0
- package/src/styles/globals.css +25 -0
- package/src/tailwind/preset.default.js +15 -0
- package/src/tailwind/tailwind.config.js +126 -0
- package/src/util/arrayUtil.js +3 -0
- package/src/util/fieldMapper.js +19 -0
- package/src/util/filterUtil.js +195 -0
- package/src/util/loading.js +17 -0
- package/src/util/localStorageUtil.js +27 -0
- package/src/util/mapIconUtil.js +179 -0
- package/src/util/mapUtil.js +91 -0
- package/src/util/page-head.js +62 -0
- package/src/util/provider.js +12 -0
- package/src/util/sortUtil.js +33 -0
- package/src/util/stringUtils.js +6 -0
- package/src/util/urlFilterUtil.js +91 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Fragment, useRef, useState, useEffect } from "react";
|
|
2
|
+
import { Combobox, Transition } from "@headlessui/react";
|
|
3
|
+
import Button from "~/components/modules/buttons/default";
|
|
4
|
+
import Icon from "~/components/modules/icon";
|
|
5
|
+
import usePlacesAutocomplete, {
|
|
6
|
+
getGeocode,
|
|
7
|
+
getLatLng
|
|
8
|
+
} from "use-places-autocomplete";
|
|
9
|
+
import { useMap } from "~/contexts/mapContext";
|
|
10
|
+
import { useMapList } from "~/contexts/mapListContext";
|
|
11
|
+
import { getStorageItem } from "~/util/localStorageUtil";
|
|
12
|
+
import React from 'react'
|
|
13
|
+
|
|
14
|
+
const FilterCommute = ({ className }) => {
|
|
15
|
+
const {
|
|
16
|
+
ready,
|
|
17
|
+
value,
|
|
18
|
+
suggestions: { status, data },
|
|
19
|
+
setValue,
|
|
20
|
+
clearSuggestions
|
|
21
|
+
} = usePlacesAutocomplete();
|
|
22
|
+
|
|
23
|
+
const [selected, setSelected] = useState(getStorageItem('selectedCommute', ''));
|
|
24
|
+
const inputRef = useRef(null);
|
|
25
|
+
const { setCommuteLocation, commuteLocation } = useMapList();
|
|
26
|
+
const [isCurrentLocation, setIsCurrentLocation] = useState(getStorageItem('isCurrentLocation', false));
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if(commuteLocation !== null && commuteLocation != '') return;
|
|
30
|
+
setIsCurrentLocation(false);
|
|
31
|
+
localStorage.removeItem('isCurrentLocation');
|
|
32
|
+
localStorage.removeItem('selectedCommute');
|
|
33
|
+
setSelected("");
|
|
34
|
+
},[commuteLocation]);
|
|
35
|
+
|
|
36
|
+
const handleSelect = async (val, isCurrLocation = false) => {
|
|
37
|
+
setValue(val, false);
|
|
38
|
+
setSelected(val);
|
|
39
|
+
localStorage.setItem('selectedCommute', val);
|
|
40
|
+
clearSuggestions();
|
|
41
|
+
if (isCurrLocation) return;
|
|
42
|
+
try {
|
|
43
|
+
const results = await getGeocode({ address: val });
|
|
44
|
+
const { lat, lng } = await getLatLng(results[0]);
|
|
45
|
+
setCommuteLocation({ lat, lng });
|
|
46
|
+
} catch (error) { }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const fetchLocation = () => {
|
|
50
|
+
if (!navigator.geolocation) {
|
|
51
|
+
console.error("Geolocation is not supported by this browser.");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
navigator.geolocation.getCurrentPosition(
|
|
55
|
+
position => {
|
|
56
|
+
setCommuteLocation({
|
|
57
|
+
lat: position.coords.latitude,
|
|
58
|
+
lng: position.coords.longitude
|
|
59
|
+
});
|
|
60
|
+
handleSelect("Current Location");
|
|
61
|
+
},
|
|
62
|
+
error => {
|
|
63
|
+
console.error("Error fetching location", error);
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={`relative ${className ?? ""}`}>
|
|
70
|
+
<label
|
|
71
|
+
htmlFor="commute"
|
|
72
|
+
className="flex items-center gap-2 mb-2 text-xs uppercase font-bold text-uiText"
|
|
73
|
+
>
|
|
74
|
+
<Icon
|
|
75
|
+
icon="ri:pin-distance-fill"
|
|
76
|
+
size="w-5 h-5"
|
|
77
|
+
className="text-uiAccent/30"
|
|
78
|
+
/>
|
|
79
|
+
<span>
|
|
80
|
+
<span className="md:hidden lg:inline">Calculate your</span> commute
|
|
81
|
+
</span>
|
|
82
|
+
</label>
|
|
83
|
+
|
|
84
|
+
<Combobox value={selected} onChange={handleSelect}>
|
|
85
|
+
<div className="relative mt-1">
|
|
86
|
+
<div className="relative flex items-center w-full pr-2 cursor-default overflow-hidden rounded bg-white text-left border border-uiAccent/20 focus-within:ring-1 focus-within:ring-uiAccent focus:outline-none">
|
|
87
|
+
<Combobox.Input
|
|
88
|
+
className="w-full border-none py-2 pl-4 pr-1 text-sm leading-5 text-gray-900 focus:ring-0 placeholder:text-gray-400"
|
|
89
|
+
onChange={e => {
|
|
90
|
+
setValue(e.target.value);
|
|
91
|
+
setSelected(e.target.value);
|
|
92
|
+
}}
|
|
93
|
+
value={selected}
|
|
94
|
+
disabled={!ready}
|
|
95
|
+
placeholder="Starting point"
|
|
96
|
+
ref={inputRef}
|
|
97
|
+
/>
|
|
98
|
+
<Button.Btn
|
|
99
|
+
variant="icon"
|
|
100
|
+
size="sqsm"
|
|
101
|
+
onClick={() => {setIsCurrentLocation(!isCurrentLocation);
|
|
102
|
+
localStorage.setItem('isCurrentLocation', !isCurrentLocation);
|
|
103
|
+
if(isCurrentLocation || commuteLocation){
|
|
104
|
+
setCommuteLocation("");
|
|
105
|
+
setSelected("");
|
|
106
|
+
}else if(!commuteLocation){fetchLocation();}}}
|
|
107
|
+
|
|
108
|
+
className=""
|
|
109
|
+
>
|
|
110
|
+
<span className=" sr-only">Use your location</span>
|
|
111
|
+
{commuteLocation ? <Icon icon="mdi:times"></Icon> : <Button.Icon className={isCurrentLocation ? 'text-blue-500' : 'text-current'} icon="mdi:my-location" />}
|
|
112
|
+
</Button.Btn>
|
|
113
|
+
</div>
|
|
114
|
+
<Transition
|
|
115
|
+
as={Fragment}
|
|
116
|
+
leave="transition ease-in duration-100"
|
|
117
|
+
leaveFrom="opacity-100"
|
|
118
|
+
leaveTo="opacity-0"
|
|
119
|
+
afterLeave={clearSuggestions}
|
|
120
|
+
>
|
|
121
|
+
<Combobox.Options className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded bg-white py-1 text-base shadow ring-1 ring-uiAccent/10 focus:outline-none sm:text-sm">
|
|
122
|
+
{status === "OK" &&
|
|
123
|
+
data.map(data => (
|
|
124
|
+
<Combobox.Option
|
|
125
|
+
key={data.place_id}
|
|
126
|
+
value={data.description}
|
|
127
|
+
className={({ active }) =>
|
|
128
|
+
`relative cursor-default select-none px-4 py-2 ${active ? "bg-primary text-white" : "text-uiText"
|
|
129
|
+
}`
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
{({ selected, active }) => (
|
|
133
|
+
<span
|
|
134
|
+
className={`block truncate ${selected ? "font-bold" : "font-medium"}`}
|
|
135
|
+
>
|
|
136
|
+
{data.description}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</Combobox.Option>
|
|
140
|
+
))}
|
|
141
|
+
</Combobox.Options>
|
|
142
|
+
</Transition>
|
|
143
|
+
</div>
|
|
144
|
+
</Combobox>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default FilterCommute;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import FilterSearch from "~/components/modules/filter/search";
|
|
3
|
+
import FiltersAccordion from "~/components/modules/accordions/filters";
|
|
4
|
+
import FilterLocations from "~/components/modules/filter/location";
|
|
5
|
+
import Button from "~/components/modules/buttons/default";
|
|
6
|
+
import { useMap } from "~/contexts/mapContext";
|
|
7
|
+
import { useMapList } from "~/contexts/mapListContext";
|
|
8
|
+
import React from 'react'
|
|
9
|
+
|
|
10
|
+
const Filter = ({
|
|
11
|
+
className,
|
|
12
|
+
showMap
|
|
13
|
+
}) => {
|
|
14
|
+
const [hasActiveFilters, setHasActiveFilters] = useState(false);
|
|
15
|
+
const [defaultValue, setDefaultValue] = useState(null);
|
|
16
|
+
const { setSelectedListItem, setLocation, filterReset } = useMap();
|
|
17
|
+
const {
|
|
18
|
+
filteredListings,
|
|
19
|
+
selectedFilters,
|
|
20
|
+
setSelectedFilters,
|
|
21
|
+
setMobileTab,
|
|
22
|
+
handleSettingFavorites,
|
|
23
|
+
setQuery,
|
|
24
|
+
siteConfig
|
|
25
|
+
} = useMapList();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={`
|
|
30
|
+
relative max-h-[95vh] md:max-h-screen overflow-y-auto overflow-x-auto
|
|
31
|
+
${className ?? ""}
|
|
32
|
+
`}
|
|
33
|
+
>
|
|
34
|
+
<div className="px-4 md:pt-4 space-y-4">
|
|
35
|
+
<FiltersAccordion
|
|
36
|
+
setHasActiveFilters={setHasActiveFilters}
|
|
37
|
+
defaultValue={defaultValue}
|
|
38
|
+
setDefaultValue={value => {setDefaultValue(value == defaultValue ? "" : value);}}
|
|
39
|
+
setLocation={setLocation}
|
|
40
|
+
setSelectedListItem={setSelectedListItem}
|
|
41
|
+
/>
|
|
42
|
+
<FilterSearch/>
|
|
43
|
+
{siteConfig.hideLocations !== true &&
|
|
44
|
+
<FilterLocations
|
|
45
|
+
setHasActiveFilters={setHasActiveFilters}
|
|
46
|
+
defaultValue={defaultValue}
|
|
47
|
+
showMap={showMap}
|
|
48
|
+
setDefaultValue={value => {setDefaultValue(value == defaultValue ? "" : value);}}
|
|
49
|
+
setLocation={setLocation}
|
|
50
|
+
setSelectedListItem={setSelectedListItem}
|
|
51
|
+
/>
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="sticky bottom-0 inset-x-0 flex items-center justify-between gap-2 py-2 px-4 mt-2 bg-white md:bg-gray-100">
|
|
55
|
+
<Button.Btn
|
|
56
|
+
onClick={() => { filterReset(); setSelectedFilters({}); setQuery(null); handleSettingFavorites(null);}}
|
|
57
|
+
variant="outline"
|
|
58
|
+
size="sm"
|
|
59
|
+
>
|
|
60
|
+
Reset
|
|
61
|
+
</Button.Btn>
|
|
62
|
+
{selectedFilters && Object.keys(selectedFilters).length > 0 &&
|
|
63
|
+
<Button.Btn
|
|
64
|
+
onClick={() => setMobileTab("listTab")}
|
|
65
|
+
variant="primary"
|
|
66
|
+
size="sm"
|
|
67
|
+
className={`
|
|
68
|
+
md:hidden
|
|
69
|
+
${hasActiveFilters ? "opacity-0 pointer-events-none" : "opacity-100"}
|
|
70
|
+
`}
|
|
71
|
+
>
|
|
72
|
+
<Button.Body>
|
|
73
|
+
<Button.Icon
|
|
74
|
+
icon="fluent:search-12-filled"
|
|
75
|
+
size="w-3.5 h-3.5"
|
|
76
|
+
/>
|
|
77
|
+
Show {filteredListings.length} Jobs
|
|
78
|
+
</Button.Body>
|
|
79
|
+
</Button.Btn>
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default Filter;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
const FilterItem = ({
|
|
5
|
+
className,
|
|
6
|
+
item,
|
|
7
|
+
type = 'checkbox',
|
|
8
|
+
itemKey = null,
|
|
9
|
+
hasCount = true,
|
|
10
|
+
field,
|
|
11
|
+
selectedFilters,
|
|
12
|
+
setSelectedFilters,
|
|
13
|
+
...rest
|
|
14
|
+
}) => {
|
|
15
|
+
const itemName = item.name ? item.name : item;
|
|
16
|
+
itemKey = itemKey === null ? itemName : itemKey;
|
|
17
|
+
var isActive =
|
|
18
|
+
selectedFilters != undefined && !!selectedFilters[field]?.[itemKey];
|
|
19
|
+
|
|
20
|
+
const changeHandler = () => {
|
|
21
|
+
setSelectedFilters(prevFilters => {
|
|
22
|
+
const updatedFilters = { ...prevFilters };
|
|
23
|
+
if (!isActive) {
|
|
24
|
+
if (!updatedFilters[field]) {
|
|
25
|
+
updatedFilters[field] = {};
|
|
26
|
+
}
|
|
27
|
+
updatedFilters[field][itemKey] = true;
|
|
28
|
+
return updatedFilters;
|
|
29
|
+
}
|
|
30
|
+
delete updatedFilters[field][itemKey];
|
|
31
|
+
if (Object.keys(updatedFilters[field]).length === 0) {
|
|
32
|
+
delete updatedFilters[field];
|
|
33
|
+
}
|
|
34
|
+
return updatedFilters;
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const [activeItem, setActiveItem] = useState(isActive);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if(selectedFilters && selectedFilters[field] && Object.keys(selectedFilters[field])?.length > 0) return;
|
|
41
|
+
else if(activeItem === true){
|
|
42
|
+
setActiveItem(false);
|
|
43
|
+
}
|
|
44
|
+
},[selectedFilters]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<label
|
|
48
|
+
className={`
|
|
49
|
+
flex items-start gap-2 px-2 py-1.5 rounded-sm text-sm cursor-pointer transition hover:bg-uiAccent/5
|
|
50
|
+
${className ?? ""}
|
|
51
|
+
`}
|
|
52
|
+
{...rest}
|
|
53
|
+
>
|
|
54
|
+
<input
|
|
55
|
+
id={itemKey}
|
|
56
|
+
name={field}
|
|
57
|
+
disabled={item.count == 0}
|
|
58
|
+
value={itemName}
|
|
59
|
+
type={type}
|
|
60
|
+
className={`h-4 w-4 mt-px text-primary border-uiAccent/30 transition-colors rounded-sm`}
|
|
61
|
+
checked = {activeItem }
|
|
62
|
+
onChange={() => {
|
|
63
|
+
setActiveItem(!activeItem);
|
|
64
|
+
changeHandler();
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
<span className="font-medium">{itemName}</span>
|
|
68
|
+
{hasCount && (
|
|
69
|
+
<span className="inline-block mt-1 ml-auto text-xs leading-none text-primary">
|
|
70
|
+
({item.count})
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
</label>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default FilterItem;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Accordion from "~/components/modules/accordions/default";
|
|
2
|
+
import FilterItem from "~/components/modules/filter/item";
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import FilterCommute from "~/components/modules/filter/commute";
|
|
5
|
+
import FilterPointsOfInterest from "~/components/modules/filter/points-of-interest";
|
|
6
|
+
import ItemsPill from "~/components/modules/buttons/items-pill";
|
|
7
|
+
import FilterCard from "~/components/modules/cards/filter";
|
|
8
|
+
import Loading from "~/util/loading";
|
|
9
|
+
import AccordionFilterItem from "~/components/modules/accordions/filterItem";
|
|
10
|
+
import { useMapList } from "~/contexts/mapListContext";
|
|
11
|
+
|
|
12
|
+
const FilterLocation = ({
|
|
13
|
+
className,
|
|
14
|
+
defaultValue,
|
|
15
|
+
setDefaultValue,
|
|
16
|
+
showMap = false,
|
|
17
|
+
setLocation,
|
|
18
|
+
setSelectedListItem
|
|
19
|
+
}) => {
|
|
20
|
+
const { setSelectedFilters, selectedFilters, filterOptions } = useMapList();
|
|
21
|
+
const activeItemsCount = selectedFilters != null && selectedFilters["cityState"]
|
|
22
|
+
? Object.keys(selectedFilters["cityState"]).length
|
|
23
|
+
: 0;
|
|
24
|
+
|
|
25
|
+
const handleClearFilters = () => {
|
|
26
|
+
setSelectedFilters(prevFilters => {
|
|
27
|
+
const updatedFilters = { ...prevFilters };
|
|
28
|
+
delete updatedFilters["cityState"];
|
|
29
|
+
return updatedFilters;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
var locations = filterOptions?.locations;
|
|
33
|
+
var pointsOfInterest = filterOptions?.pointsOfInterest;
|
|
34
|
+
return (
|
|
35
|
+
<FilterCard className={className ?? ""}>
|
|
36
|
+
<FilterCard.Title icon="fluent:location-16-filled">
|
|
37
|
+
<span>
|
|
38
|
+
<span className="hidden lg:inline">Job</span> location
|
|
39
|
+
</span>
|
|
40
|
+
</FilterCard.Title>
|
|
41
|
+
{!locations && (
|
|
42
|
+
<Loading />
|
|
43
|
+
)}
|
|
44
|
+
{locations && <Accordion defaultValue={defaultValue} className="space-y-4">
|
|
45
|
+
{locations?.map(filter =>
|
|
46
|
+
(<AccordionFilterItem
|
|
47
|
+
key={filter.id}
|
|
48
|
+
filter={filter}
|
|
49
|
+
setDefaultValue={setDefaultValue}
|
|
50
|
+
selectedFilters={selectedFilters}
|
|
51
|
+
setSelectedFilters={prevFilters => { setSelectedFilters(prevFilters); setLocation(null); setSelectedListItem(null); }}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
{showMap && <FilterCommute className="mt-6" />}
|
|
55
|
+
{showMap && (
|
|
56
|
+
<FilterPointsOfInterest
|
|
57
|
+
className="mt-4"
|
|
58
|
+
pointsOfInterest={pointsOfInterest}
|
|
59
|
+
setDefaultValue={setDefaultValue}
|
|
60
|
+
defaultValue={defaultValue}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
</Accordion>
|
|
64
|
+
}
|
|
65
|
+
</FilterCard>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default FilterLocation;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Accordion from "~/components/modules/accordions/default";
|
|
2
|
+
import RadioItem from "~/components/modules/filter/radio-item";
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
const FilterPointsOfInterest = ({
|
|
6
|
+
title = "Points of interest",
|
|
7
|
+
pointsOfInterest,
|
|
8
|
+
setDefaultValue,
|
|
9
|
+
className,
|
|
10
|
+
defaultValue
|
|
11
|
+
}) => {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={`
|
|
15
|
+
relative
|
|
16
|
+
${className ?? ""}
|
|
17
|
+
`}
|
|
18
|
+
>
|
|
19
|
+
<Accordion defaultValue={defaultValue} className="space-y-4">
|
|
20
|
+
<Accordion.Item id="points-of-interest">
|
|
21
|
+
<Accordion.Trigger.HasHeader
|
|
22
|
+
onClick={() => setDefaultValue("points-of-interest")}
|
|
23
|
+
className="stretched-link text-left"
|
|
24
|
+
iconClassName="order-last"
|
|
25
|
+
headerClassName="relative py-2 rounded border border-uiAccent/20 bg-white text-sm transition data-[state=open]:border-b-transparent data-[state=open]:rounded-b-none"
|
|
26
|
+
>
|
|
27
|
+
{title}
|
|
28
|
+
</Accordion.Trigger.HasHeader>
|
|
29
|
+
<Accordion.Content bodyClassName="px-2 py-1 bg-white rounded-b border border-uiAccent/20 border-t-0 max-h-[20vh] md:max-h-[25vh] overflow-auto">
|
|
30
|
+
{pointsOfInterest.items.map((item, index) => {
|
|
31
|
+
return <RadioItem
|
|
32
|
+
key={item.key + index}
|
|
33
|
+
item={item}
|
|
34
|
+
field={item.id} />;
|
|
35
|
+
})}
|
|
36
|
+
</Accordion.Content>
|
|
37
|
+
</Accordion.Item>
|
|
38
|
+
</Accordion>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default FilterPointsOfInterest;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Icon from "~/components/modules/icon";
|
|
2
|
+
import { useMap } from "~/contexts/mapContext";
|
|
3
|
+
import React from 'react'
|
|
4
|
+
|
|
5
|
+
const RadioItem = ({
|
|
6
|
+
className,
|
|
7
|
+
item,
|
|
8
|
+
field,
|
|
9
|
+
...rest
|
|
10
|
+
}) => {
|
|
11
|
+
const { selectedPlaces, setSelectedPlaces } = useMap();
|
|
12
|
+
const chosenPlace = item.key.toLowerCase();
|
|
13
|
+
let isActive = selectedPlaces.includes(chosenPlace);
|
|
14
|
+
|
|
15
|
+
const changeHandler = () => {
|
|
16
|
+
setSelectedPlaces([ isActive ? '' : chosenPlace]);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<label key ={item.key}
|
|
21
|
+
className={`
|
|
22
|
+
flex items-start gap-2 px-2 py-1.5 rounded-sm text-sm cursor-pointer transition hover:bg-uiAccent/5
|
|
23
|
+
${item.count === 0 ? 'text-uiDisabled cursor-not-allowed' : 'hover:bg-uiAccent/5'}
|
|
24
|
+
${className ?? ""}
|
|
25
|
+
`}
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
<input
|
|
29
|
+
key ={item.key}
|
|
30
|
+
id={item.Key}
|
|
31
|
+
name={field}
|
|
32
|
+
value={item.key}
|
|
33
|
+
type={"radio"}
|
|
34
|
+
className={`h-4 w-4 mt-px text-primary border-uiAccent/30 transition-colors rounded-full`}
|
|
35
|
+
checked={isActive}
|
|
36
|
+
onClick={() => {
|
|
37
|
+
changeHandler();
|
|
38
|
+
}}
|
|
39
|
+
onChange={()=> {}}
|
|
40
|
+
/>
|
|
41
|
+
<span className="font-medium">{item.key}</span>
|
|
42
|
+
{isActive &&
|
|
43
|
+
<div className=" w-full unselect-div">
|
|
44
|
+
<Icon className="float-right" icon="mdi:times"></Icon>
|
|
45
|
+
</div>
|
|
46
|
+
}
|
|
47
|
+
</label>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default RadioItem;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from "react";
|
|
2
|
+
import Button from "~/components/modules/buttons/default";
|
|
3
|
+
import Icon from "~/components/modules/icon";
|
|
4
|
+
import FilterCard from "~/components/modules/cards/filter";
|
|
5
|
+
import { useMapList } from "~/contexts/mapListContext";
|
|
6
|
+
import React from 'react'
|
|
7
|
+
|
|
8
|
+
const Search = ({
|
|
9
|
+
inputPlaceholder = "Keywords",
|
|
10
|
+
showSearchIcon = false,
|
|
11
|
+
className,
|
|
12
|
+
labelClassName
|
|
13
|
+
}) => {
|
|
14
|
+
const [inputValue, setInputValue] = useState(query != null ? query: "");
|
|
15
|
+
const debounceTimer = useRef(null);
|
|
16
|
+
const {
|
|
17
|
+
query,
|
|
18
|
+
setQuery,
|
|
19
|
+
siteConfig
|
|
20
|
+
} = useMapList();
|
|
21
|
+
const handleInputChange = e => {
|
|
22
|
+
setInputValue(e.target.value);
|
|
23
|
+
if (debounceTimer.current) {
|
|
24
|
+
clearTimeout(debounceTimer.current);
|
|
25
|
+
}
|
|
26
|
+
debounceTimer.current = setTimeout(() => {
|
|
27
|
+
setQuery(e.target.value);
|
|
28
|
+
}, 500);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleReset = () => {
|
|
32
|
+
setInputValue("");
|
|
33
|
+
setQuery("");
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
useEffect(() =>{
|
|
37
|
+
if(query == null){
|
|
38
|
+
setInputValue("");
|
|
39
|
+
}
|
|
40
|
+
},[query]);
|
|
41
|
+
inputPlaceholder = siteConfig.searchConfig.placeholder;
|
|
42
|
+
return (
|
|
43
|
+
<FilterCard as="form" className={className ?? ""}>
|
|
44
|
+
<FilterCard.Title
|
|
45
|
+
as="label"
|
|
46
|
+
icon="fluent:search-12-filled"
|
|
47
|
+
className={labelClassName ?? ""}
|
|
48
|
+
>
|
|
49
|
+
<span>
|
|
50
|
+
{siteConfig.searchConfig.label}
|
|
51
|
+
</span>
|
|
52
|
+
</FilterCard.Title>
|
|
53
|
+
|
|
54
|
+
<div className="relative flex items-center px-2 rounded bg-white border border-uiAccent/20 focus-within:ring-1 focus-within:ring-uiAccent">
|
|
55
|
+
{showSearchIcon && (
|
|
56
|
+
<Icon
|
|
57
|
+
icon="fluent:search-12-filled"
|
|
58
|
+
size="w-4 h-4"
|
|
59
|
+
className="mr-2 text-uiAccent/50"
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<input
|
|
63
|
+
type="text"
|
|
64
|
+
name="search"
|
|
65
|
+
onKeyDown={e => { if (e.key === 'Enter') {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
}}}
|
|
68
|
+
placeholder={inputPlaceholder}
|
|
69
|
+
value={inputValue}
|
|
70
|
+
className="w-full px-0 py-2 text-sm border-0 transition-colors placeholder:text-uiText/50 focus:ring-0 focus:outline-none"
|
|
71
|
+
onChange={handleInputChange}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<Button.Btn
|
|
75
|
+
type="reset"
|
|
76
|
+
variant="icon"
|
|
77
|
+
size="sqsm"
|
|
78
|
+
className={`transition-opacity ${inputValue ? "opacity-100" : "opacity-0 pointer-events-none"}`}
|
|
79
|
+
onClick={handleReset}
|
|
80
|
+
>
|
|
81
|
+
<span className="sr-only">Clear</span>
|
|
82
|
+
<Button.Icon icon="uil:times" className="text-uiAccent" />
|
|
83
|
+
</Button.Btn>
|
|
84
|
+
</div>
|
|
85
|
+
</FilterCard>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export default Search;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { forwardRef } from "react";
|
|
2
|
+
import * as Select from "@radix-ui/react-select";
|
|
3
|
+
import { twMerge } from "tailwind-merge";
|
|
4
|
+
import Icon from "~/components/modules/icon";
|
|
5
|
+
import Button from "~/components/modules/buttons/default";
|
|
6
|
+
import React from 'react'
|
|
7
|
+
|
|
8
|
+
const FilterSort = ({ className, fields, setSortSetting, fieldNames }) => {
|
|
9
|
+
const handleSortChange = value => {
|
|
10
|
+
const [field, direction] = value.split("-");
|
|
11
|
+
setSortSetting({ field, type: direction });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={className ?? ""}>
|
|
16
|
+
<Select.Root onValueChange={handleSortChange}>
|
|
17
|
+
<Select.Trigger asChild aria-label="Sort">
|
|
18
|
+
<Button.Btn variant="outline" size="sm" className="normal-case">
|
|
19
|
+
<Button.Body className="justify-center">
|
|
20
|
+
<Select.Value placeholder={
|
|
21
|
+
<span className="flex items-center gap-2">
|
|
22
|
+
Sort
|
|
23
|
+
<Icon icon="mdi:sort" />
|
|
24
|
+
</span>
|
|
25
|
+
}/>
|
|
26
|
+
<Select.Icon>
|
|
27
|
+
<Button.Icon icon="mdi:chevron-down" />
|
|
28
|
+
</Select.Icon>
|
|
29
|
+
</Button.Body>
|
|
30
|
+
</Button.Btn>
|
|
31
|
+
</Select.Trigger>
|
|
32
|
+
<Select.Portal>
|
|
33
|
+
<Select.Content className={`relative z-[200] overflow-hidden bg-white rounded-md `}>
|
|
34
|
+
<Select.ScrollUpButton className="flex items-center justify-center h-5 bg-white text-primary cursor-default">
|
|
35
|
+
<Icon icon="mdi:chevron-up" />
|
|
36
|
+
</Select.ScrollUpButton>
|
|
37
|
+
<Select.Viewport className="p-1">
|
|
38
|
+
{fields.map(field => (
|
|
39
|
+
<Select.Group key={field}>
|
|
40
|
+
<SelectItem value={`${field}-asc`}>
|
|
41
|
+
<span className="flex items-center justify-between gap-2 w-full">
|
|
42
|
+
{fieldNames[field] ?? field}
|
|
43
|
+
<Icon icon="mdi:sort-ascending" />
|
|
44
|
+
</span>
|
|
45
|
+
</SelectItem>
|
|
46
|
+
<SelectItem value={`${field}-desc`}>
|
|
47
|
+
<span className="flex items-center justify-between gap-2 w-full">
|
|
48
|
+
{fieldNames[field] ?? field}
|
|
49
|
+
<Icon icon="mdi:sort-descending" />
|
|
50
|
+
</span>
|
|
51
|
+
</SelectItem>
|
|
52
|
+
</Select.Group>
|
|
53
|
+
))}
|
|
54
|
+
</Select.Viewport>
|
|
55
|
+
<Select.ScrollDownButton className="flex items-center justify-center h-5 bg-white text-primary cursor-default">
|
|
56
|
+
<Icon icon="mdi:chevron-down" />
|
|
57
|
+
</Select.ScrollDownButton>
|
|
58
|
+
</Select.Content>
|
|
59
|
+
</Select.Portal>
|
|
60
|
+
</Select.Root>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const SelectItem = forwardRef(({ children, className, ...props }, forwardedRef) => (
|
|
66
|
+
<Select.Item
|
|
67
|
+
className={twMerge(
|
|
68
|
+
"text-sm leading-none rounded-sm flex items-center w-full py-1.5 pr-4 pl-7 relative select-none cursor-pointer data-[disabled]:text-gray-500 data-[disabled]:pointer-events-none data-[highlighted]:outline-none data-[highlighted]:bg-primary data-[highlighted]:text-white",
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
ref={forwardedRef}
|
|
73
|
+
>
|
|
74
|
+
<Select.ItemText>{children}</Select.ItemText>
|
|
75
|
+
<Select.ItemIndicator className="absolute left-0 w-8 inline-flex items-center justify-center">
|
|
76
|
+
<Icon icon="mdi:check" />
|
|
77
|
+
</Select.ItemIndicator>
|
|
78
|
+
</Select.Item>
|
|
79
|
+
));
|
|
80
|
+
|
|
81
|
+
SelectItem.displayName = "SelectItem";
|
|
82
|
+
|
|
83
|
+
export default FilterSort;
|