@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
package/src/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import PageHead from '~/util/page-head';
|
|
3
|
+
import Grid from '~/components/modules/grid';
|
|
4
|
+
import Filter from '~/components/modules/filter';
|
|
5
|
+
import MapList from '~/components/modules/maps/map-list';
|
|
6
|
+
import { useLoadScript } from '@react-google-maps/api';
|
|
7
|
+
import { MapProvider } from '~/contexts/mapContext';
|
|
8
|
+
import { PlacesProvider } from '~/contexts/placesContext';
|
|
9
|
+
import { MapListProvider } from '~/contexts/mapListContext';
|
|
10
|
+
import { BrowserRouter as Router } from 'react-router-dom';
|
|
11
|
+
import './index.css';
|
|
12
|
+
import RootLayout from './components/layout/layout';
|
|
13
|
+
import { getMapConfig } from '~/services/configService';
|
|
14
|
+
import { setClientAuthKey } from '~/clientToken.js';
|
|
15
|
+
|
|
16
|
+
const libraries = ['places'];
|
|
17
|
+
|
|
18
|
+
export const HireControlMap = ({ clientToken }) => {
|
|
19
|
+
const [siteConfig, setSiteconfig] = useState(null);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setClientAuthKey(clientToken);
|
|
23
|
+
const fetchSiteConfig = async () => {
|
|
24
|
+
try {
|
|
25
|
+
const configData = await getMapConfig(clientToken);
|
|
26
|
+
setSiteconfig(configData);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('Failed to fetch site configuration:', error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
fetchSiteConfig();
|
|
33
|
+
|
|
34
|
+
const handlePopState = (event) => {
|
|
35
|
+
window.location.reload();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
window.addEventListener('popstate', handlePopState);
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('popstate', handlePopState);
|
|
42
|
+
};
|
|
43
|
+
}, [clientToken]);
|
|
44
|
+
|
|
45
|
+
const { isLoaded } = useLoadScript({
|
|
46
|
+
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY,
|
|
47
|
+
libraries: libraries,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Router>
|
|
52
|
+
<RootLayout>
|
|
53
|
+
{isLoaded && siteConfig && (
|
|
54
|
+
<HomeBody siteConfig={siteConfig} />
|
|
55
|
+
)}
|
|
56
|
+
</RootLayout>
|
|
57
|
+
</Router>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const HomeBody = ({ siteConfig }) => {
|
|
62
|
+
const resetFilters = false;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<MapListProvider siteConfig={siteConfig} resetFilters={resetFilters}>
|
|
66
|
+
<MapProvider resetFilters={resetFilters}>
|
|
67
|
+
<PlacesProvider
|
|
68
|
+
placeMappings={siteConfig.pointsOfInterestConfig.placeMappings ?? {}}
|
|
69
|
+
markerColors={{
|
|
70
|
+
fillColor: siteConfig.colors.primary,
|
|
71
|
+
strokeColor: siteConfig.colors.primaryDark,
|
|
72
|
+
selectedFillColor: siteConfig.colors.secondary,
|
|
73
|
+
selectedStrokeColor: siteConfig.colors.secondaryDark,
|
|
74
|
+
placeMarkers: {
|
|
75
|
+
colors: siteConfig.pointsOfInterestConfig.placeMarkerColors,
|
|
76
|
+
size: siteConfig.pointsOfInterestConfig.placeMarkerSize,
|
|
77
|
+
},
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<PageHead title='' />
|
|
81
|
+
<Grid
|
|
82
|
+
as='section'
|
|
83
|
+
id='job-search-interface'
|
|
84
|
+
columns='md:grid-cols-[1fr_2.5fr] lg:grid-cols-[1fr_3.5fr]'
|
|
85
|
+
gap='gap-0'
|
|
86
|
+
autoRows={false}
|
|
87
|
+
className='items-stretch h-screen min-h-[30rem] divide-x divide-uiAccent/20'
|
|
88
|
+
>
|
|
89
|
+
<Grid.Item className='bg-gray-100'>
|
|
90
|
+
<Filter showMap={siteConfig.showMap} className='hidden md:block' />
|
|
91
|
+
</Grid.Item>
|
|
92
|
+
<MapList
|
|
93
|
+
markerConfigs={{
|
|
94
|
+
fillColor: siteConfig.colors.primary,
|
|
95
|
+
strokeColor: siteConfig.colors.primaryDark,
|
|
96
|
+
selectedFillColor: siteConfig.colors.secondary,
|
|
97
|
+
selectedStrokeColor: siteConfig.colors.secondaryDark,
|
|
98
|
+
placeMarkers: {
|
|
99
|
+
colors: siteConfig.pointsOfInterestConfig.placeMarkerColors,
|
|
100
|
+
size: siteConfig.pointsOfInterestConfig.placeMarkerSize,
|
|
101
|
+
},
|
|
102
|
+
}}
|
|
103
|
+
showMap={siteConfig.showMap}
|
|
104
|
+
fieldsShown={siteConfig.fieldsShown}
|
|
105
|
+
specialFeatures={siteConfig.specialFeatures}
|
|
106
|
+
fieldNames={siteConfig.fieldNames}
|
|
107
|
+
placeMappings={siteConfig.pointsOfInterestConfig.placeMappings ?? {}}
|
|
108
|
+
/>
|
|
109
|
+
</Grid>
|
|
110
|
+
</PlacesProvider>
|
|
111
|
+
</MapProvider>
|
|
112
|
+
</MapListProvider>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default HireControlMap;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import api from "../apis/hcApi";
|
|
2
|
+
|
|
3
|
+
export const getMapConfig = async () => {
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const response = await api.get(`/MapConfig`);
|
|
7
|
+
return response;
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error("Error retrieving map configuration:", error);
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
getMapConfig
|
|
16
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const searchNearbyPlaces = async (typesArray, location, radius) => {
|
|
2
|
+
const url = 'https://places.googleapis.com/v1/places:searchNearby';
|
|
3
|
+
const headers = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
'X-Goog-Api-Key': process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY,
|
|
6
|
+
'X-Goog-FieldMask': 'places.location,places.displayName,places.types'
|
|
7
|
+
};
|
|
8
|
+
const data = {
|
|
9
|
+
includedTypes: typesArray,
|
|
10
|
+
maxResultCount: 20,
|
|
11
|
+
locationRestriction: {
|
|
12
|
+
circle: {
|
|
13
|
+
center: location,
|
|
14
|
+
radius: radius
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: headers,
|
|
23
|
+
body: JSON.stringify(data)
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
return await response.json();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error making the Nearby Search request:', error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getListings } from "./listingService";
|
|
2
|
+
import { getRecruiters } from "./recruiterService";
|
|
3
|
+
import { getListingEntities } from "./listingEntityService";
|
|
4
|
+
import { getDistinctItemsByProximity } from "../util/mapUtil";
|
|
5
|
+
|
|
6
|
+
const fetchListings = async (query, siteConfig) => {
|
|
7
|
+
try {
|
|
8
|
+
const listingsResult = await getListings(
|
|
9
|
+
siteConfig.companyId,
|
|
10
|
+
5000,
|
|
11
|
+
1,
|
|
12
|
+
query,
|
|
13
|
+
);
|
|
14
|
+
const recruiterIds = [
|
|
15
|
+
...new Set(listingsResult.map(listing => listing.recruiterId))
|
|
16
|
+
];
|
|
17
|
+
const fetchedRecruiters = await getRecruiters(recruiterIds);
|
|
18
|
+
|
|
19
|
+
const distinctEntityIds = [
|
|
20
|
+
...new Set(listingsResult.map(listing => listing.entityId))
|
|
21
|
+
];
|
|
22
|
+
const fetchedEntities = await getListingEntities(
|
|
23
|
+
distinctEntityIds
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const distinctItems = getDistinctItemsByProximity(
|
|
27
|
+
listingsResult,
|
|
28
|
+
fetchedEntities,
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
listingsResult,
|
|
32
|
+
fetchedRecruiters,
|
|
33
|
+
fetchedEntities,
|
|
34
|
+
distinctItems
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Error fetching listings:", error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default fetchListings;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import api from "../apis/hcApi";
|
|
2
|
+
export const getListingEntities = async (entityIds, origin = '') => {
|
|
3
|
+
try {
|
|
4
|
+
const response = await api.post(`/ListingEntities?origin=${origin}`, entityIds);
|
|
5
|
+
return response;
|
|
6
|
+
} catch (error) {
|
|
7
|
+
console.error("Error fetching listing entities:", error);
|
|
8
|
+
throw error;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
getListingEntities
|
|
14
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import api from "../apis/hcApi";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const getListings = async companyId => {
|
|
5
|
+
try {
|
|
6
|
+
var x = '';
|
|
7
|
+
const response = await api.get(`/Listings`);
|
|
8
|
+
return response;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(error);
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getListingDetails = async listingId => {
|
|
16
|
+
try {
|
|
17
|
+
const response = await api.get(`/ListingDetails/${listingId}`);
|
|
18
|
+
return response;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
getListings,
|
|
27
|
+
getListingDetails
|
|
28
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import api from "../apis/hcApi";
|
|
2
|
+
|
|
3
|
+
export const getRecruiters = async recruiterIds => {
|
|
4
|
+
try {
|
|
5
|
+
const params = recruiterIds.map(id => `recruiterIds=${id}`).join("&");
|
|
6
|
+
|
|
7
|
+
const response = await api.get(`/Recruiters?${params}`);
|
|
8
|
+
return response.data;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error("Error fetching recruiters:", error);
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
getRecruiters
|
|
17
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
@tailwind base;
|
|
3
|
+
@tailwind components;
|
|
4
|
+
@tailwind utilities;
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/* @layer base {
|
|
8
|
+
html {
|
|
9
|
+
@apply text-400 text-uiText [scroll-behavior:smooth];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.track * {
|
|
15
|
+
@apply pointer-events-none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.stretched-link::after {
|
|
19
|
+
@apply content-[''] absolute inset-0 z-[1] pointer-events-auto bg-transparent;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.fit-content{
|
|
24
|
+
height:fit-content;
|
|
25
|
+
} */
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const defaultTheme = require("tailwindcss/defaultTheme");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
content: [
|
|
5
|
+
'../**/*.{js,ts,jsx,tsx,mdx}',
|
|
6
|
+
'../components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
7
|
+
],
|
|
8
|
+
presets: [require(`./preset.default.js`)],
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
animation: {
|
|
12
|
+
slideDown: "slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)",
|
|
13
|
+
slideUp: "slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1)",
|
|
14
|
+
overlayShow: "overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1)",
|
|
15
|
+
contentShow: "contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)",
|
|
16
|
+
},
|
|
17
|
+
container: {
|
|
18
|
+
center: true,
|
|
19
|
+
},
|
|
20
|
+
cursor: {
|
|
21
|
+
grab: "grab",
|
|
22
|
+
},
|
|
23
|
+
maxHeight: {
|
|
24
|
+
"45vh": "45vh",
|
|
25
|
+
},
|
|
26
|
+
colors: {
|
|
27
|
+
gray: {
|
|
28
|
+
850: "#141414",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
fontFamily: {
|
|
32
|
+
sans: ["var(--font-roboto-flex)", ...defaultTheme.fontFamily.sans],
|
|
33
|
+
},
|
|
34
|
+
fontSize: {
|
|
35
|
+
"2xs": "0.65rem",
|
|
36
|
+
300: "clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem)",
|
|
37
|
+
400: "clamp(0.88rem, 0.83rem + 0.24vw, 1rem)",
|
|
38
|
+
450: "clamp(1rem, 1rem + 0.33vw, 1.33rem)",
|
|
39
|
+
500: "clamp(1.09rem, 1rem + 0.47vw, 1.33rem)",
|
|
40
|
+
600: "clamp(1.37rem, 1.21rem + 0.8vw, 1.78rem)",
|
|
41
|
+
700: "clamp(1.71rem, 1.45rem + 1.29vw, 2.37rem)",
|
|
42
|
+
800: "clamp(2.14rem, 1.74rem + 1.99vw, 3.16rem)",
|
|
43
|
+
900: "clamp(2.67rem, 2.07rem + 3vw, 4.21rem)",
|
|
44
|
+
1000: "clamp(3.34rem, 2.45rem + 4.43vw, 5.61rem)",
|
|
45
|
+
},
|
|
46
|
+
keyframes: {
|
|
47
|
+
slideDown: {
|
|
48
|
+
from: { height: 0 },
|
|
49
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
50
|
+
},
|
|
51
|
+
slideUp: {
|
|
52
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
53
|
+
to: { height: 0 },
|
|
54
|
+
},
|
|
55
|
+
overlayShow: {
|
|
56
|
+
from: { opacity: 0 },
|
|
57
|
+
to: { opacity: 1 },
|
|
58
|
+
},
|
|
59
|
+
contentShow: {
|
|
60
|
+
from: { opacity: 0, transform: "scale(0.96)" },
|
|
61
|
+
to: { opacity: 1, transform: "scale(1)" },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
typography: {
|
|
65
|
+
DEFAULT: {
|
|
66
|
+
css: {
|
|
67
|
+
video: {
|
|
68
|
+
"margin-bottom": 0,
|
|
69
|
+
"margin-top": 0,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
sm: {
|
|
74
|
+
css: {
|
|
75
|
+
video: {
|
|
76
|
+
"margin-bottom": 0,
|
|
77
|
+
"margin-top": 0,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
md: {
|
|
82
|
+
css: {
|
|
83
|
+
video: {
|
|
84
|
+
"margin-bottom": 0,
|
|
85
|
+
"margin-top": 0,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
lg: {
|
|
90
|
+
css: {
|
|
91
|
+
video: {
|
|
92
|
+
"margin-bottom": 0,
|
|
93
|
+
"margin-top": 0,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
xl: {
|
|
98
|
+
css: {
|
|
99
|
+
video: {
|
|
100
|
+
"margin-bottom": 0,
|
|
101
|
+
"margin-top": 0,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
"2xl": {
|
|
106
|
+
css: {
|
|
107
|
+
video: {
|
|
108
|
+
"margin-bottom": 0,
|
|
109
|
+
"margin-top": 0,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
zIndex: {
|
|
115
|
+
"-1": "-1",
|
|
116
|
+
1: "1",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
plugins: [
|
|
121
|
+
require("@tailwindcss/aspect-ratio"),
|
|
122
|
+
require("@tailwindcss/forms"),
|
|
123
|
+
require("@tailwindcss/typography"),
|
|
124
|
+
require("tailwindcss-animate"),
|
|
125
|
+
],
|
|
126
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Grid from "~/components/modules/grid";
|
|
3
|
+
import { capitalize } from "./stringUtils";
|
|
4
|
+
const mapFieldsToGridItems = (item, fieldsShown) => {
|
|
5
|
+
const orderedFields = fieldsShown.filter(field => field in item.fields);
|
|
6
|
+
|
|
7
|
+
return orderedFields.map(field => {
|
|
8
|
+
let value = item.fields[field];
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Grid.Item key={field}>
|
|
12
|
+
<span className="sr-only">{capitalize(field)}</span>
|
|
13
|
+
{value}
|
|
14
|
+
</Grid.Item>
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default mapFieldsToGridItems;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { getDistinctItemsByProximity } from "./mapUtil";
|
|
2
|
+
|
|
3
|
+
export const getFilterOptions = (listings, filteredListings, field, excludeZeroCount = null) => {
|
|
4
|
+
const options = new Set();
|
|
5
|
+
listings.forEach(listing => {
|
|
6
|
+
if (listing.fields[field]) {
|
|
7
|
+
options.add(listing.fields[field]);
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const optionCounts = {};
|
|
12
|
+
options.forEach(option => {
|
|
13
|
+
optionCounts[option] = 0;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
filteredListings.forEach(listing => {
|
|
17
|
+
const value = listing.fields[field];
|
|
18
|
+
if (value && optionCounts.hasOwnProperty(value)) {
|
|
19
|
+
optionCounts[value] += 1;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return Array.from(options)
|
|
24
|
+
.sort()
|
|
25
|
+
.map(option => ({
|
|
26
|
+
name: option,
|
|
27
|
+
count: optionCounts[option] || 0
|
|
28
|
+
}))
|
|
29
|
+
.filter(option => !(excludeZeroCount === true && option.count === 0));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig, favorites) => {
|
|
33
|
+
const specialFeatures = siteConfig.specialFeatures;
|
|
34
|
+
const featureCounts = Object.keys(specialFeatures).sort().reduce((acc, key) => {
|
|
35
|
+
acc[specialFeatures[key]] = 0;
|
|
36
|
+
return acc;
|
|
37
|
+
}, {});
|
|
38
|
+
|
|
39
|
+
filteredListings.forEach(listing => {
|
|
40
|
+
Object.entries(specialFeatures).forEach(([featureKey, featureName]) => {
|
|
41
|
+
if (listing.fields[featureKey] === 1) {
|
|
42
|
+
featureCounts[featureName] += 1;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const specialFeatureOptions = Object.entries(featureCounts).map(([name, count]) => ({
|
|
48
|
+
name,
|
|
49
|
+
count
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
for (let option of specialFeatureOptions) {
|
|
53
|
+
if (option.name === 'Favorite') {
|
|
54
|
+
option.count = filteredListings.filter(x => favorites.includes(x.id)).length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return specialFeatureOptions;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getPointsOfInterestOptions = pointsOfInterestNames => {
|
|
62
|
+
return Object.entries(pointsOfInterestNames).sort().map(([key, name]) => ({
|
|
63
|
+
key,
|
|
64
|
+
name
|
|
65
|
+
}));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const generateFilterOptions = (
|
|
69
|
+
filteredListings,
|
|
70
|
+
allListings,
|
|
71
|
+
siteConfig,
|
|
72
|
+
filterOptions,
|
|
73
|
+
parentField = null,
|
|
74
|
+
favorites
|
|
75
|
+
) => {
|
|
76
|
+
if (allListings.length > 0) {
|
|
77
|
+
const dynamicFilters = siteConfig.fieldFiltersShown.map(fieldName => {
|
|
78
|
+
if (fieldName === parentField && filterOptions?.filters) {
|
|
79
|
+
return filterOptions.filters.find(filter => filter.id === fieldName);
|
|
80
|
+
}
|
|
81
|
+
if(fieldName == "specialFeatures"){
|
|
82
|
+
return {
|
|
83
|
+
id: fieldName,
|
|
84
|
+
title: siteConfig.fieldNames[fieldName],
|
|
85
|
+
items: getSpecialFeatureOptions(allListings, filteredListings, siteConfig, favorites).sort()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
id: fieldName,
|
|
90
|
+
title: siteConfig.fieldNames[fieldName],
|
|
91
|
+
items: getFilterOptions(allListings, filteredListings, fieldName)
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const locations =
|
|
96
|
+
siteConfig.locationFiltersShown.map((fieldName, index) => {
|
|
97
|
+
if(index === 0 && filterOptions?.locations){
|
|
98
|
+
return filterOptions.locations.find(filter => filter.id === fieldName);
|
|
99
|
+
}
|
|
100
|
+
return{
|
|
101
|
+
id: fieldName,
|
|
102
|
+
title: siteConfig.fieldNames[fieldName],
|
|
103
|
+
items: getFilterOptions(allListings, filteredListings, fieldName, true)
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const pointsOfInterest = {
|
|
108
|
+
id: "pointsOfInterest",
|
|
109
|
+
title: siteConfig.pointsOfInterestConfig.title,
|
|
110
|
+
items: getPointsOfInterestOptions(
|
|
111
|
+
siteConfig.pointsOfInterestConfig.pointsOfInterestNames,
|
|
112
|
+
)
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
filters: dynamicFilters,
|
|
117
|
+
locations: locations,
|
|
118
|
+
pointsOfInterest: pointsOfInterest
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const applyFilters = (
|
|
126
|
+
allListings,
|
|
127
|
+
selectedFilters,
|
|
128
|
+
query,
|
|
129
|
+
listingEntities,
|
|
130
|
+
favorites,
|
|
131
|
+
siteConfig
|
|
132
|
+
) => {
|
|
133
|
+
let results = allListings;
|
|
134
|
+
let invertedSpecialFeaturesMap;
|
|
135
|
+
if(siteConfig.specialFeatures){
|
|
136
|
+
invertedSpecialFeaturesMap = Object.entries(siteConfig.specialFeatures).reduce((acc, [key, value]) => {
|
|
137
|
+
acc[value] = key;
|
|
138
|
+
return acc;
|
|
139
|
+
}, {});
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
const hasFavorite = !!selectedFilters.specialFeatures && !!selectedFilters.specialFeatures.Favorite;
|
|
143
|
+
|
|
144
|
+
if(hasFavorite && selectedFilters.specialFeatures.Favorite == true){
|
|
145
|
+
results = results.filter(x => favorites.includes(x.id));
|
|
146
|
+
}
|
|
147
|
+
var favorite;
|
|
148
|
+
if(hasFavorite){
|
|
149
|
+
favorite = selectedFilters.specialFeatures.Favorite;
|
|
150
|
+
delete selectedFilters.specialFeatures.Favorite;
|
|
151
|
+
}
|
|
152
|
+
for (const [field, filterItems] of Object.entries(selectedFilters)) {
|
|
153
|
+
const formattedField = field;
|
|
154
|
+
if (field === "pointsOfInterest") continue;
|
|
155
|
+
if (field === "specialFeatures" && invertedSpecialFeaturesMap && Object.keys(filterItems).length > 0) {
|
|
156
|
+
results = results.filter(listing => {
|
|
157
|
+
return Object.entries(filterItems).some(([filterName, filterValue]) => {
|
|
158
|
+
const listingFieldName = invertedSpecialFeaturesMap[filterName];
|
|
159
|
+
return filterValue && listing.fields[listingFieldName] === 1;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
} else if (Object.keys(filterItems).length > 0) {
|
|
163
|
+
results = results.filter(listing =>
|
|
164
|
+
filterItems.hasOwnProperty(listing.fields[formattedField]),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (query) {
|
|
169
|
+
results = results.filter(listing =>
|
|
170
|
+
Object.values(listing.fields)?.some(value =>
|
|
171
|
+
value?.toString().toLowerCase().includes(query.toLowerCase()),
|
|
172
|
+
),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const distinctItems = getDistinctItemsByProximity(results, listingEntities);
|
|
176
|
+
if(hasFavorite){
|
|
177
|
+
selectedFilters.specialFeatures.Favorite = favorite;
|
|
178
|
+
}
|
|
179
|
+
return { filteredListings: results, mapItems: distinctItems };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const filterListingsByLocation = (
|
|
183
|
+
allListings,
|
|
184
|
+
selectedLocation,
|
|
185
|
+
listingEntities,
|
|
186
|
+
) => {
|
|
187
|
+
let results = allListings;
|
|
188
|
+
if (selectedLocation !== null) {
|
|
189
|
+
results = results.filter(item =>
|
|
190
|
+
selectedLocation.items.hasOwnProperty(item.id),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const mapItems = getDistinctItemsByProximity(results, listingEntities);
|
|
194
|
+
return { filteredListings: results, mapItems: mapItems };
|
|
195
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Icon from '~/components/modules/icon';
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
const Loading = ({ color = '#c0c0c0' }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex items-center justify-center w-full h-full">
|
|
7
|
+
<Icon
|
|
8
|
+
icon="ph:spinner"
|
|
9
|
+
className="animate-spin text-gray-300"
|
|
10
|
+
width="40"
|
|
11
|
+
height="40"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default Loading;
|