@abcagency/hc-ui-components 1.9.9 → 1.9.12
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/components/containers/accordions/filter-container.js +1 -1
- package/dist/components/containers/accordions/filter-container.js.map +1 -1
- package/dist/components/modules/list/field-mapper-desktop.js +7 -3
- package/dist/components/modules/list/field-mapper-desktop.js.map +1 -1
- package/dist/components/modules/list/field-mapper-mobile.js +6 -4
- package/dist/components/modules/list/field-mapper-mobile.js.map +1 -1
- package/dist/contexts/mapListContext.js +2 -4
- package/dist/contexts/mapListContext.js.map +1 -1
- package/dist/types/util/filterUtil.d.ts +3 -1
- package/dist/util/filterUtil.js +157 -25
- package/dist/util/filterUtil.js.map +1 -1
- package/package.json +1 -1
- package/src/components/containers/accordions/filter-container.js +1 -1
- package/src/components/modules/list/field-mapper-desktop.jsx +9 -3
- package/src/components/modules/list/field-mapper-mobile.jsx +6 -4
- package/src/contexts/mapListContext.tsx +41 -43
- package/src/util/fieldMapper.js +6 -2
- package/src/util/filterUtil.js +128 -12
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { createContext, useState, useEffect, useContext, useRef, ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
import { generateFilterOptions, applyFilters, filterListingsByLocation } from '~/util/filterUtil';
|
|
3
|
+
import { generateFilterOptions, applyFilters, filterListingsByLocation, getFieldValue } from '~/util/filterUtil';
|
|
4
4
|
import { getStorageObject, setStorageObject } from '~/util/localStorageUtil';
|
|
5
5
|
import { updateURLWithFilters, filtersFromURL } from '~/util/urlFilterUtil';
|
|
6
6
|
|
|
@@ -223,13 +223,13 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
223
223
|
if (setFiltersUrl === true && urlData?.query) {
|
|
224
224
|
return urlData.query;
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
// Only fall back to localStorage if there's NO query param in URL
|
|
228
228
|
// This prevents localStorage from overriding empty query params
|
|
229
229
|
if (setFiltersUrl === true && typeof urlData?.query !== 'undefined') {
|
|
230
230
|
return null; // URL has query param but it's empty/null
|
|
231
231
|
}
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
// Fall back to localStorage only if URL has no query param at all
|
|
234
234
|
return getQuery(localStorageKey);
|
|
235
235
|
}
|
|
@@ -252,17 +252,17 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
252
252
|
const storedMapItems = getStorageObject(localStorageKey + 'mapItems', []) || [];
|
|
253
253
|
const storedSortSetting = getStorageObject(localStorageKey + 'sortSetting', { field: 'position', type: 'asc' }) || { field: 'position', type: 'asc' };
|
|
254
254
|
const storedCommuteLocation = getStorageObject(localStorageKey + 'commuteLocation');
|
|
255
|
-
|
|
255
|
+
|
|
256
256
|
setMapItems(storedMapItems);
|
|
257
257
|
setSortSetting(storedSortSetting);
|
|
258
258
|
if (storedCommuteLocation) setCommuteLocation(storedCommuteLocation);
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
// Load filters and query
|
|
261
261
|
if (!resetFilters) {
|
|
262
262
|
setSelectedFilters(firstLoadFilters());
|
|
263
263
|
setQuery(firstLoadQuery());
|
|
264
264
|
}
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
setHasMounted(true);
|
|
267
267
|
}
|
|
268
268
|
}, [localStorageKey]);
|
|
@@ -305,11 +305,11 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
305
305
|
if (!getListingEntitiesCallback) {
|
|
306
306
|
return;
|
|
307
307
|
}
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
const fetchedEntities = await getListingEntitiesCallback(`${commuteLocation.lat}, ${commuteLocation.lng}`);
|
|
310
310
|
console.log('Fetched entities with travel times:', fetchedEntities);
|
|
311
311
|
setListingEntities(fetchedEntities);
|
|
312
|
-
|
|
312
|
+
|
|
313
313
|
// Update travelTime on ALL listings (both allListings and filteredListings)
|
|
314
314
|
const updatedAllListings = allListings.map(listing => {
|
|
315
315
|
if (
|
|
@@ -333,7 +333,7 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
333
333
|
}
|
|
334
334
|
return listing;
|
|
335
335
|
});
|
|
336
|
-
|
|
336
|
+
|
|
337
337
|
console.log('Updated listings with travel times:', updatedAllListings.slice(0, 2));
|
|
338
338
|
setAllListings(updatedAllListings);
|
|
339
339
|
} catch (error) {
|
|
@@ -349,9 +349,9 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
349
349
|
// Note: Commute location changes will fetch updated entities with travel times via separate useEffect
|
|
350
350
|
useEffect(() => {
|
|
351
351
|
if (!entities || entitiesInitialized.current) return;
|
|
352
|
-
|
|
352
|
+
|
|
353
353
|
entitiesInitialized.current = true;
|
|
354
|
-
|
|
354
|
+
|
|
355
355
|
// Handle both object (production format) and array (for backwards compatibility)
|
|
356
356
|
if (Array.isArray(entities)) {
|
|
357
357
|
// Convert array to object
|
|
@@ -389,44 +389,42 @@ export const MapListProvider: React.FC<MapListProviderProps> = ({
|
|
|
389
389
|
let processedListings = listings;
|
|
390
390
|
if (defaultFilters) {
|
|
391
391
|
processedListings = listings.filter(listing => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const listingValue = listing.fields ? listing.fields[filterKey as keyof typeof listing.fields] : null;
|
|
397
|
-
return filterValues.includes(listingValue);
|
|
398
|
-
});
|
|
392
|
+
return Object.keys(defaultFilters).every(filterKey => {
|
|
393
|
+
const filterValues = defaultFilters[filterKey as keyof typeof defaultFilters];
|
|
394
|
+
const listingValue = getFieldValue(listing, filterKey);
|
|
395
|
+
return filterValues.includes(listingValue);
|
|
399
396
|
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
setAllListings(processedListings);
|
|
403
|
-
console.log('Set allListings to', processedListings.length, 'items');
|
|
404
|
-
|
|
405
|
-
// Map items will be set when entities are processed
|
|
406
|
-
// For now, just set empty object - will be populated when entities arrive
|
|
407
|
-
setMapItems({});
|
|
408
|
-
} catch (error) {
|
|
409
|
-
console.error('Error processing listings:', error);
|
|
397
|
+
});
|
|
410
398
|
}
|
|
411
|
-
|
|
412
|
-
setLoading(false);
|
|
413
|
-
}, [listings]);
|
|
414
399
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
400
|
+
setAllListings(processedListings);
|
|
401
|
+
console.log('Set allListings to', processedListings.length, 'items');
|
|
402
|
+
|
|
403
|
+
// Map items will be set when entities are processed
|
|
404
|
+
// For now, just set empty object - will be populated when entities arrive
|
|
405
|
+
setMapItems({});
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error('Error processing listings:', error);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
setLoading(false);
|
|
411
|
+
}, [listings]);
|
|
412
|
+
|
|
413
|
+
useEffect(() => {
|
|
414
|
+
const processListings = async () => {
|
|
415
|
+
// Don't process if allListings hasn't been loaded yet
|
|
416
|
+
if (allListings.length === 0) {
|
|
417
|
+
console.log('processListings: Skipping - allListings is empty');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
422
420
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
let filteredListings: Listing[];
|
|
422
|
+
let tempSelectedFilters = selectedFilters;
|
|
423
|
+
let tempQuery = query;
|
|
426
424
|
|
|
427
|
-
|
|
425
|
+
console.log('processListings: Running with query:', tempQuery, 'and', allListings.length, 'listings');
|
|
428
426
|
|
|
429
|
-
|
|
427
|
+
const { mapItems, filteredListings: tempFilteredListings } = await applyFilters(
|
|
430
428
|
allListings,
|
|
431
429
|
tempSelectedFilters,
|
|
432
430
|
tempQuery,
|
package/src/util/fieldMapper.js
CHANGED
|
@@ -3,12 +3,16 @@ import React from 'react';
|
|
|
3
3
|
import Grid from '~/components/modules/grid';
|
|
4
4
|
|
|
5
5
|
import { capitalize } from '~/util/stringUtils';
|
|
6
|
+
import { getFieldValue } from '~/util/filterUtil';
|
|
6
7
|
|
|
7
8
|
const mapFieldsToGridItems = (item, fieldsShown) => {
|
|
8
|
-
const orderedFields = fieldsShown.filter(field =>
|
|
9
|
+
const orderedFields = fieldsShown.filter(field => {
|
|
10
|
+
const value = getFieldValue(item, field);
|
|
11
|
+
return value !== undefined && value !== null;
|
|
12
|
+
});
|
|
9
13
|
|
|
10
14
|
return orderedFields.map(field => {
|
|
11
|
-
let value = item
|
|
15
|
+
let value = getFieldValue(item, field);
|
|
12
16
|
|
|
13
17
|
return (
|
|
14
18
|
<Grid.Item key={field}>
|
package/src/util/filterUtil.js
CHANGED
|
@@ -4,11 +4,32 @@ import { isAlgoliaAvailable, isAlgoliaInitializing, filterListingsByAlgoliaSearc
|
|
|
4
4
|
|
|
5
5
|
import Fuse from 'fuse.js';
|
|
6
6
|
|
|
7
|
+
// Helper function to get field value from either fields or customFields
|
|
8
|
+
export const getFieldValue = (listing, fieldKey) => {
|
|
9
|
+
// First check in fields
|
|
10
|
+
if (listing.fields && listing.fields[fieldKey] !== undefined && listing.fields[fieldKey] !== null) {
|
|
11
|
+
return listing.fields[fieldKey];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Then check in customFields array
|
|
15
|
+
if (listing.customFields && Array.isArray(listing.customFields)) {
|
|
16
|
+
const customField = listing.customFields.find(
|
|
17
|
+
cf => cf.id === fieldKey || cf.name === fieldKey
|
|
18
|
+
);
|
|
19
|
+
if (customField) {
|
|
20
|
+
return customField.value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
7
27
|
export const getFilterOptions = (listings, filteredListings, field, excludeZeroCount = false) => {
|
|
8
28
|
const options = new Set();
|
|
9
29
|
listings.forEach(listing => {
|
|
10
|
-
|
|
11
|
-
|
|
30
|
+
const value = getFieldValue(listing, field);
|
|
31
|
+
if (value) {
|
|
32
|
+
options.add(value);
|
|
12
33
|
}
|
|
13
34
|
});
|
|
14
35
|
|
|
@@ -18,8 +39,7 @@ export const getFilterOptions = (listings, filteredListings, field, excludeZeroC
|
|
|
18
39
|
});
|
|
19
40
|
|
|
20
41
|
filteredListings.forEach(listing => {
|
|
21
|
-
|
|
22
|
-
const value = listing.fields[field];
|
|
42
|
+
const value = getFieldValue(listing, field);
|
|
23
43
|
if (value && Object.prototype.hasOwnProperty.call(optionCounts, value)) {
|
|
24
44
|
optionCounts[value] += 1;
|
|
25
45
|
}
|
|
@@ -48,7 +68,9 @@ export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig,
|
|
|
48
68
|
|
|
49
69
|
filteredListings.forEach(listing => {
|
|
50
70
|
Object.entries(specialFeatures).forEach(([featureKey, featureName]) => {
|
|
51
|
-
|
|
71
|
+
const value = getFieldValue(listing, featureKey);
|
|
72
|
+
// Support both numeric (1/0) and string ("true"/"false") boolean values
|
|
73
|
+
if (value == 1 || value === 1 || value === '1' || value === 'true' || value === true) {
|
|
52
74
|
featureCounts[featureName] += 1;
|
|
53
75
|
}
|
|
54
76
|
});
|
|
@@ -79,6 +101,75 @@ export const getSpecialFeatureOptions = (listings, filteredListings, siteConfig,
|
|
|
79
101
|
return specialFeatureOptions;
|
|
80
102
|
};
|
|
81
103
|
|
|
104
|
+
// Helper to detect if a custom field should be treated as boolean or multi-value
|
|
105
|
+
const isCustomFieldBoolean = (listings, fieldKey) => {
|
|
106
|
+
const distinctValues = new Set();
|
|
107
|
+
|
|
108
|
+
for (const listing of listings) {
|
|
109
|
+
const value = getFieldValue(listing, fieldKey);
|
|
110
|
+
if (value != null && value !== '' && value !== undefined) {
|
|
111
|
+
distinctValues.add(String(value).toLowerCase());
|
|
112
|
+
}
|
|
113
|
+
// If we find more than 2 non-empty distinct values, it's not boolean
|
|
114
|
+
if (distinctValues.size > 2) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if all values are boolean-like
|
|
120
|
+
const booleanValues = new Set(['true', 'false', '1', '0', 'yes', 'no']);
|
|
121
|
+
for (const val of distinctValues) {
|
|
122
|
+
if (!booleanValues.has(val)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return distinctValues.size > 0 && distinctValues.size <= 2;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Get dynamic custom field filters (for non-boolean multi-value fields)
|
|
131
|
+
export const getDynamicCustomFieldFilters = (listings, filteredListings, siteConfig, filterConfig = {}) => {
|
|
132
|
+
if (!siteConfig.specialFeatures) return [];
|
|
133
|
+
|
|
134
|
+
const dynamicFilters = [];
|
|
135
|
+
const specialFeaturesKeys = Object.keys(siteConfig.specialFeatures);
|
|
136
|
+
|
|
137
|
+
// Find all custom fields used across listings
|
|
138
|
+
const allCustomFields = new Set();
|
|
139
|
+
listings.forEach(listing => {
|
|
140
|
+
if (listing.customFields && Array.isArray(listing.customFields)) {
|
|
141
|
+
listing.customFields.forEach(cf => {
|
|
142
|
+
if (cf.id || cf.name) {
|
|
143
|
+
allCustomFields.add(cf.id || cf.name);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// For each custom field, check if it's NOT in specialFeatures and NOT boolean
|
|
150
|
+
allCustomFields.forEach(fieldKey => {
|
|
151
|
+
// Skip if already in specialFeatures
|
|
152
|
+
if (specialFeaturesKeys.includes(fieldKey)) return;
|
|
153
|
+
|
|
154
|
+
// Check if this field has multiple non-boolean values
|
|
155
|
+
if (!isCustomFieldBoolean(listings, fieldKey)) {
|
|
156
|
+
const options = getFilterOptions(listings, filteredListings, fieldKey, filterConfig.hideZeroResults);
|
|
157
|
+
|
|
158
|
+
// Only add if there are meaningful options
|
|
159
|
+
if (options.length > 1) {
|
|
160
|
+
dynamicFilters.push({
|
|
161
|
+
id: fieldKey,
|
|
162
|
+
title: fieldKey.charAt(0).toUpperCase() + fieldKey.slice(1).replace(/([A-Z])/g, ' $1').trim(),
|
|
163
|
+
items: options,
|
|
164
|
+
isDynamic: true
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return dynamicFilters;
|
|
171
|
+
};
|
|
172
|
+
|
|
82
173
|
const getPointsOfInterestOptions = pointsOfInterestNames => {
|
|
83
174
|
return Object.entries(pointsOfInterestNames).sort().map(([key, name]) => ({
|
|
84
175
|
key,
|
|
@@ -111,10 +202,25 @@ export const generateFilterOptions = (
|
|
|
111
202
|
return filterOptions.filters.find(filter => filter.id === fieldName);
|
|
112
203
|
}
|
|
113
204
|
if(fieldName == 'category'){
|
|
205
|
+
// For category counts, exclude specialFeatures from the filter calculation
|
|
206
|
+
// so that toggling specialFeatures updates category counts
|
|
207
|
+
let categoryCountListings = countListings;
|
|
208
|
+
if (filterConfig.dynamicCounts && selectedFilters.specialFeatures) {
|
|
209
|
+
categoryCountListings = allListings.filter(listing => {
|
|
210
|
+
return Object.entries(selectedFilters).every(([key, value]) => {
|
|
211
|
+
if (key === 'specialFeatures') return true; // Exclude specialFeatures
|
|
212
|
+
const fieldValue = getFieldValue(listing, key);
|
|
213
|
+
if (value && typeof value === 'object' && value[fieldValue] === true) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
114
220
|
return {
|
|
115
221
|
id: fieldName,
|
|
116
222
|
title: siteConfig.fieldNames[fieldName],
|
|
117
|
-
items: getFilterOptions(allListings,
|
|
223
|
+
items: getFilterOptions(allListings, categoryCountListings, fieldName, filterConfig.hideZeroResults)
|
|
118
224
|
};
|
|
119
225
|
}
|
|
120
226
|
if(fieldName == 'subCategory' && selectedFilters.category){
|
|
@@ -146,7 +252,8 @@ export const generateFilterOptions = (
|
|
|
146
252
|
locationFilteredListings = allListings.filter(listing => {
|
|
147
253
|
return Object.entries(selectedFilters).every(([key, value]) => {
|
|
148
254
|
if (siteConfig.locationFiltersShown.includes(key)) return true;
|
|
149
|
-
|
|
255
|
+
const fieldValue = getFieldValue(listing, key);
|
|
256
|
+
if (value && typeof value === 'object' && value[fieldValue] === true) {
|
|
150
257
|
return true;
|
|
151
258
|
}
|
|
152
259
|
return false;
|
|
@@ -211,6 +318,9 @@ export const generateFilterOptions = (
|
|
|
211
318
|
)
|
|
212
319
|
};
|
|
213
320
|
|
|
321
|
+
// Get dynamic custom field filters (multi-value fields not in specialFeatures)
|
|
322
|
+
const dynamicCustomFilters = getDynamicCustomFieldFilters(allListings, countListings, siteConfig, filterConfig);
|
|
323
|
+
|
|
214
324
|
// Filter out null filters and ensure items arrays don't contain undefined values
|
|
215
325
|
const cleanFilters = dynamicFilters
|
|
216
326
|
.filter(f => f != null && f.items != null)
|
|
@@ -226,8 +336,11 @@ export const generateFilterOptions = (
|
|
|
226
336
|
items: l.items.filter(item => item != null && item.name != null)
|
|
227
337
|
}));
|
|
228
338
|
|
|
339
|
+
// Add dynamic custom field filters to the main filters array
|
|
340
|
+
const allCleanFilters = [...cleanFilters, ...dynamicCustomFilters];
|
|
341
|
+
|
|
229
342
|
return {
|
|
230
|
-
filters:
|
|
343
|
+
filters: allCleanFilters,
|
|
231
344
|
locations: cleanLocations,
|
|
232
345
|
pointsOfInterest: pointsOfInterest
|
|
233
346
|
};
|
|
@@ -271,13 +384,16 @@ export const applyFilters = async (
|
|
|
271
384
|
results = results.filter(listing => {
|
|
272
385
|
return Object.entries(filterItems).some(([filterName, filterValue]) => {
|
|
273
386
|
const listingFieldName = invertedSpecialFeaturesMap[filterName];
|
|
274
|
-
|
|
387
|
+
const value = getFieldValue(listing, listingFieldName);
|
|
388
|
+
// Support both numeric (1/0) and string ("true"/"false") boolean values
|
|
389
|
+
return filterValue && (value == 1 || value === 1 || value === '1' || value === 'true' || value === true);
|
|
275
390
|
});
|
|
276
391
|
});
|
|
277
392
|
} else if (Object.keys(filterItems).length > 0) {
|
|
278
|
-
results = results.filter(listing =>
|
|
279
|
-
|
|
280
|
-
|
|
393
|
+
results = results.filter(listing => {
|
|
394
|
+
const value = getFieldValue(listing, formattedField);
|
|
395
|
+
return Object.prototype.hasOwnProperty.call(filterItems, value);
|
|
396
|
+
});
|
|
281
397
|
}
|
|
282
398
|
}
|
|
283
399
|
if (query) {
|